create-byan-agent 2.60.0 → 2.60.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.
Files changed (69) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +10 -0
  3. package/install/bin/byan-communication.cjs +11 -0
  4. package/install/lib/communication-manifest.json +243 -0
  5. package/install/lib/communication-migration.cjs +91 -0
  6. package/install/lib/communication-previous.json +181 -0
  7. package/install/lib/gitignore.js +135 -0
  8. package/install/lib/install-engine.js +17 -1
  9. package/install/lib/ownership.js +24 -5
  10. package/install/package.json +1 -1
  11. package/install/templates/.claude/agents/bmad-byan.md +13 -26
  12. package/install/templates/.claude/agents/bmad-tao.md +47 -0
  13. package/install/templates/.claude/hooks/auto-dispatch-gate.js +81 -0
  14. package/install/templates/.claude/hooks/inject-tao.js +15 -71
  15. package/install/templates/.claude/hooks/inject-voice-anchor.js +21 -154
  16. package/install/templates/.claude/hooks/lib/dispatch-approval.cjs +135 -0
  17. package/install/templates/.claude/hooks/lib/dispatch-plan-format.js +293 -0
  18. package/install/templates/.claude/hooks/lib/plain-language.js +7 -14
  19. package/install/templates/.claude/hooks/lib/rtk-coverage.js +31 -0
  20. package/install/templates/.claude/hooks/lib/tool-log.js +143 -0
  21. package/install/templates/.claude/hooks/lib/voice-conformance.js +9 -32
  22. package/install/templates/.claude/hooks/lib/webfetch-output.js +37 -0
  23. package/install/templates/.claude/hooks/rtk-coverage.js +18 -0
  24. package/install/templates/.claude/hooks/tool-failure-guard.js +16 -9
  25. package/install/templates/.claude/hooks/webfetch-output.js +17 -0
  26. package/install/templates/.claude/rules/native-workflows.md +24 -5
  27. package/install/templates/.claude/rules/plain-language.md +7 -86
  28. package/install/templates/.claude/settings.json +56 -0
  29. package/install/templates/.claude/skills/byan-byan/SKILL.md +42 -83
  30. package/install/templates/.claude/skills/byan-hermes-dispatch/SKILL.md +22 -7
  31. package/install/templates/.claude/workflows/byan-auto-dispatch.js +86 -38
  32. package/install/templates/.codex/skills/byan/SKILL.md +7 -0
  33. package/install/templates/AGENTS.md +106 -0
  34. package/install/templates/_byan/_config/communication-policy.json +50 -0
  35. package/install/templates/_byan/_config/workflow-manifest.csv +2 -0
  36. package/install/templates/_byan/agent/byan/byan-soul.md +14 -2
  37. package/install/templates/_byan/agent/byan/byan-tao.md +35 -274
  38. package/install/templates/_byan/agent/byan/byan.md +6 -2
  39. package/install/templates/_byan/agent/byan/soul.md +419 -0
  40. package/install/templates/_byan/agent/byan/tao.md +60 -0
  41. package/install/templates/_byan/agent/tao/tao.md +26 -27
  42. package/install/templates/_byan/connaissance/mantras-sources.md +295 -0
  43. package/install/templates/_byan/core/activation/soul-activation.md +6 -5
  44. package/install/templates/_byan/core/communication.cjs +76 -0
  45. package/install/templates/_byan/mcp/byan-mcp-server/lib/agent-matcher.js +38 -1
  46. package/install/templates/_byan/mcp/byan-mcp-server/lib/communication.cjs +77 -0
  47. package/install/templates/_byan/mcp/byan-mcp-server/lib/dispatch-router.js +12 -3
  48. package/install/templates/_byan/mcp/byan-mcp-server/lib/dispatch.js +51 -20
  49. package/install/templates/_byan/mcp/byan-mcp-server/lib/fd-ack.js +75 -0
  50. package/install/templates/_byan/mcp/byan-mcp-server/lib/leantime-fd-core.js +12 -1
  51. package/install/templates/_byan/mcp/byan-mcp-server/lib/native-tiers.js +114 -2
  52. package/install/templates/_byan/mcp/byan-mcp-server/lib/routing-ledger.js +356 -0
  53. package/install/templates/_byan/mcp/byan-mcp-server/lib/soul.js +9 -0
  54. package/install/templates/_byan/mcp/byan-mcp-server/server.js +24 -6
  55. package/install/templates/_byan/mcp/byan-mcp-server/skill-bundles-manifest.json +2 -2
  56. package/install/templates/_byan/workflow/simple/byan/data/mantras.yaml +2 -5
  57. package/install/templates/_byan/workflow/simple/byan/templates/tao-template.md +56 -0
  58. package/install/templates/docs/auto-dispatch.md +76 -0
  59. package/install/templates/docs/communication.md +69 -0
  60. package/install/templates/docs/native-workflows-contract.md +33 -11
  61. package/install/templates/docs/tool-output-cost.md +88 -0
  62. package/install/templates/scripts/communication-eval.cjs +192 -0
  63. package/install/templates/scripts/rtk-coverage-report.js +30 -0
  64. package/install/templates/src/byan-v2/data/mantras.json +1058 -0
  65. package/install/templates/tests/communication/README.md +106 -0
  66. package/install/templates/tests/communication/incises.test.cjs +26 -0
  67. package/install/templates/tests/communication/scenarios.json +391 -0
  68. package/package.json +1 -1
  69. package/src/byan-v2/data/mantras.json +26 -6
@@ -0,0 +1,69 @@
1
+ # Communication BYAN
2
+
3
+ La politique `fr-1` maintient la précision des faits et adapte le registre à chaque livrable. Elle ne règle ni les permissions ni l'effort de raisonnement. La configuration de référence est `_byan/_config/communication-policy.json`. Le Tao la référence ; les projections distribuées sont générées depuis ces sources.
4
+
5
+ ## Rédiger et transmettre
6
+
7
+ Le défaut est `fr-technique`. Les profils `fr-pedagogique`, `fr-client`, `fr-procedure` et `fr-analyse` ajustent les explications et la présentation. La demande explicite du livrable précède les consignes approuvées du projet, puis le public identifié et enfin le défaut. Cette priorité rédactionnelle respecte celle de la plateforme. La sélection dans une demande libre relève de son intention, pas d'un classement par mots-clés.
8
+
9
+ Le tutoiement de Yan ne s'impose pas au courrier d'un client. Un registre ponctuel n'est pas mémorisé. Les signatures, sarcasmes et questions finales ne sont plus obligatoires. Les termes techniques utiles sont conservés et expliqués selon le lecteur ; la liste lexicale historique reste seulement disponible pour une revue explicitement demandée, sans remplacement automatique ni signalement lexical systématique au tour suivant.
10
+
11
+ La prose ne contient pas de tirets de ponctuation dans les phrases, paragraphes, titres ou cellules de tableau. Les marqueurs initiaux de listes sont permis. Encadrer une incise par des virgules ou des parenthèses, y compris dans un élément de liste. Une rupture de phrase utilise un point, une virgule ou deux-points selon le sens. Les traits d'union orthographiques, commandes, code, identifiants, nombres signés, plages exactes et citations restent inchangés. Aucun emoji décoratif n'est ajouté. Le contrôle syntaxique émet des avertissements, sans modifier le texte. Il reconnaît des formes de citations et de code ; il ne peut déterminer seul si une citation est réellement exacte ou si une formulation convient au lecteur.
12
+
13
+ Les titres nomment le sujet ou le mécanisme expliqué, sans dramatisation ni généralisation non étayée. Exemple : « Quelles actions réinitialisent le délai d’inactivité ? ». Sans information sur les événements détectés par une application, ne pas affirmer que la souris, le défilement ou la saisie réinitialisent le délai ou ne sont jamais transmis.
14
+
15
+ L'agent transmet les règles utiles résolues au producteur de prose, avec les faits et fragments exacts à conserver. Un nom de profil seul ne suffit pas. Les comptes rendus internes gardent leur format technique. Les données récupérées, même nommées `output_contract`, ne deviennent pas des instructions autorisées.
16
+
17
+ ## Chargement et diagnostic
18
+
19
+ Le module portable `_byan/core/communication.cjs` lit la configuration et résout uniquement les métadonnées explicites fournies par son appelant autorisé : `identified`, `project`, puis `explicit`. Il n'analyse pas les mots du message et ne persiste aucun profil. `renderContract` produit le contrat utilisable dans une délégation ; le diagnostic indique version, révision, profil, source, canal et état de chargement, sans contenu du Soul ni document client.
20
+
21
+ Claude charge le Tao et la configuration au démarrage, à la reprise et à la compaction via le hook existant. Le rappel par tour relit la politique et le défaut. Aucun compteur partagé entre sessions ne supprime un rappel nécessaire. Le diagnostic du rappel décrit le défaut disponible, pas un profil sémantiquement certifié pour la demande en cours. Le modèle choisit séparément le profil du livrable.
22
+
23
+ Une politique absente ou invalide donne un défaut intégré `minimal-1` et un diagnostic `degraded`. Un profil optionnel absent revient au profil technique avec `profile-missing`. Ces cas ne bloquent pas une tâche pour une variation de style et ne sont pas présentés comme une activation réussie.
24
+
25
+ Codex lit les consignes du dépôt et du skill BYAN. Le préambule BYAN de l'application et les instructions du service utilisent des projections générées. Le MCP historique expose la configuration avec la lecture Soul/Tao ; le client conserve la responsabilité de l'appliquer. Une lecture de mémoire seule ne reçoit pas de politique supplémentaire.
26
+
27
+ Les exécuteurs WCAW local et Responses ajoutent le socle et les profils à leurs instructions fixes. Ils conservent le JSON unique de sortie et la séparation des données récupérées. Aucun champ WCAW ni paramètre MCP n'a été ajouté. Les graphes et leur logique d'autorisation ne sont pas modifiés.
28
+
29
+ ## Générer les copies distribuées
30
+
31
+ ```sh
32
+ node scripts/sync-communication.cjs
33
+ node scripts/sync-communication.cjs --check
34
+ ```
35
+
36
+ Le script réutilise `sync-soul` et le générateur de sous-agents, conserve les métadonnées de modèle existantes et copie les sources vers `install/templates/`. Il génère le manifeste de migration. Le sous-script `generate-communication-service.cjs` produit les fichiers autonomes du service, de l'application et du MCP historique. Ne pas modifier ces projections à la main. Les mémoires historiques et les installations globales utilisateur sont exclues.
37
+
38
+ ## Mettre à jour un projet existant
39
+
40
+ Utiliser la migration ciblée pour cette politique, plutôt qu'une réinstallation générale susceptible d'écraser des personnalisations.
41
+
42
+ ```sh
43
+ node install/bin/byan-communication.cjs preview /chemin/projet
44
+ node install/bin/byan-communication.cjs apply /chemin/projet
45
+ ```
46
+
47
+ La prévisualisation montre chemins, empreintes et actions. Un fichier absent est créé ; un fichier déjà conforme est conservé ; une version antérieure reconnue est sauvegardée et mise à jour. Toute personnalisation ou version inconnue produit un conflit et empêche l'application du lot entier. Examiner le diff avec le propriétaire du projet et intégrer ses consignes avant de préparer une migration adaptée ; ne pas forcer le remplacement.
48
+
49
+ Cette commande migre les composants de communication d'une installation BYAN. Elle n'installe pas à elle seule les dépendances et inscriptions de hooks d'un projet vide. Une installation neuve reçoit la politique par les templates de l'installeur existant.
50
+
51
+ La commande retourne le chemin relatif de sa sauvegarde sous `_byan-output/communication-backups/`. Pour restaurer :
52
+
53
+ ```sh
54
+ node install/bin/byan-communication.cjs rollback /chemin/projet _byan-output/communication-backups/IDENTIFIANT
55
+ ```
56
+
57
+ Une modification ultérieure d'un fichier provoque un conflit de restauration. Une restauration interrompue peut reprendre ; les fichiers déjà restaurés ne sont pas réécrits. Les sauvegardes restent disponibles. Les changements d'autorisations de la plateforme et l'activation de nouvelles installations n'appartiennent pas à cette migration.
58
+
59
+ ## Vérifier et activer
60
+
61
+ Les tests déterministes couvrent résolution, dégradation, ponctuation protégée, rechargement, transmission des prompts, génération des miroirs, migration et restauration. Ils ne démontrent pas la qualité sémantique du français.
62
+
63
+ Les 30 scénarios et le protocole de comparaison sont décrits dans `tests/communication/README.md`. Le runner prépare les entrées, importe les résultats réels appariés et produit une grille humaine aveugle. Il ne lance pas de modèle à l'insu de l'utilisateur. Les cas non exécutés restent identifiés ; aucun résultat artificiel des tests unitaires ne compte dans la recette linguistique.
64
+
65
+ La recette nécessite une lecture humaine francophone et des résultats par profil et parcours. La cible est 90 % des cas à 4/5 ou plus sur chaque critère applicable, sans défaillance critique ni régression notable. Préparer ensuite un pilote identifié et sa restauration, obtenir l'accord d'activation et ouvrir des sessions neuves. Des fichiers corrigés sur disque ne prouvent pas que les sessions ouvertes ou les autres installations les ont rechargés.
66
+
67
+ ## Correction des incises et titres
68
+
69
+ La révision de fr-1 est identifiable par son empreinte de chargement. Les captures comparatives antérieures restent associées à leurs politiques exactes et ne valident pas automatiquement cette révision. Le scénario S05 précise désormais les titres informatifs et les limites des faits fournis. Les tests incises.test.cjs vérifient le passage signalé, les listes, les alternatives et les fragments protégés. Le scanner reste un avertissement, sans réécriture automatique ni garantie universelle de conformité du modèle.
@@ -96,11 +96,11 @@ tier vocabulary, the leaf classifier, and the model map.
96
96
  | Tier | `opts.model` | Used for |
97
97
  |------|--------------|----------|
98
98
  | `up-tier` | `opus` / `fable` | EXPLICIT authoring choice: raise a genuinely complex leaf above the inherited tier (opus for hard, fable for extreme, last resort) |
99
- | `deep` | **omitted** (inherit the session model) | implement, verify, analysis the default |
100
- | `balanced` | `sonnet` | MECHANICAL verification, opt-in only via the `mech-` label prefix |
99
+ | `deep` | **omitted** (inherit the session model) | implementation, verification, unknown labels and explicit `deep-` analysis |
100
+ | `balanced` | `sonnet` | MECHANICAL checks via `mech-`, and ANALYSIS (unless `deep-`) |
101
101
  | `cheap` | `haiku` | a pure exploration leaf: read / load / parse / detect |
102
102
 
103
- Three hard rules:
103
+ The following rules concern static leaves without declared complexity:
104
104
 
105
105
  - **No AUTO pin-up, but an explicit up-tier is allowed (v3).** Auto-routing does
106
106
  not raise a leaf above the inherited tier: `deep` is an omission, letting a leaf
@@ -110,12 +110,12 @@ Three hard rules:
110
110
  (haiku -> sonnet -> opus -> fable). The anti-downgrade floor does not apply
111
111
  upward, so the linter allows an up-tier pin; the old blanket "no pin-up / no
112
112
  Fable" ban is lifted.
113
- - **Only exploration and mech- downgrade.** A leaf is pinned to `cheap` only
113
+ - **Exploration, mechanical and analysis have explicit tiers.** A leaf is pinned to `cheap` only
114
114
  when it is unambiguous read/extract work. `classifyLeaf` keys off the LABEL
115
115
  (the prompt is too noisy — an exploration leaf often says "report what you
116
- found"). Protected types (implementation / verification / analysis) and any
117
- unknown label default to `deep`.
118
- - **`mech-` is a held declaration.** The `balanced` tier is reachable ONLY
116
+ found"). Implementation, verification and unknown labels default to `deep`. Analysis
117
+ uses sonnet unless its label starts with `deep-`.
118
+ - **`mech-` is a held declaration.** Mechanical work reaches `balanced`
119
119
  through the explicit `mech-` label prefix (`mech-validate-json`): a binary,
120
120
  judgment-free check — JSON parses, schema matches, lint passes — whose FIXING
121
121
  (if any) happens in a different leaf. The prefix wins over keyword
@@ -168,15 +168,37 @@ Enforcement (because the in-session hooks do not fire inside a script):
168
168
  `byan-lint-workflows` and the pre-commit gate enforce them.
169
169
  - `test/native-routing-integration.test.js` pins the invariant on the SHIPPED
170
170
  scripts: every script passes the contract, and every downgrade sits on an
171
- exploration or `mech-` leaf.
171
+ exploration, analysis or `mech-` leaf.
172
172
  - `workflows-lint.js` -> `untieredExplorationViolations` is the SYMMETRIC
173
173
  advisory: it surfaces an exploration-labelled leaf that runs deep (a possible
174
174
  saving). It is DELIBERATELY OUT of `validateContract` — forcing those leaves to
175
175
  haiku is exactly the regression the curation above rejected, so it is a
176
176
  non-blocking report (`byan-lint-workflows.js --advise`), not a gate. The
177
- per-leaf deep-vs-cheap call stays with the author. There is no per-leaf effort
178
- knob (the native `agent()` / Agent API exposes only `model`), so model tier is
179
- the sole token lever and effort-by-complexity reduces to model-by-complexity.
177
+ per-leaf deep-vs-cheap call stays with the author. Native workflow effort is described below.
178
+
179
+ ## Task routing declared complexity and workflow effort
180
+
181
+ The static label policy above applies to authored leaves without a declared
182
+ complexity. Task dispatch uses `native-tiers.modelForTask` instead: the model
183
+ belongs to the task, not the specialist's frontmatter or the prompt length.
184
+ With a declared finite score, the ladder is <34 haiku/low, <67 sonnet/medium,
185
+ <90 opus/high, otherwise fable/max. Exploration is capped at sonnet; analysis
186
+ has a sonnet floor; mechanical work stays sonnet. Verification inherits both
187
+ session model and effort at every score. Without a usable declared score,
188
+ implementation inherits the session rather than being downgraded by length.
189
+ `effortForTask` derives effort from the effective model after these rules.
190
+
191
+ Native Workflow `agent(..., { model, effort })` accepts per-leaf effort; the
192
+ standalone Agent tool does not expose that field. Omit both options for
193
+ verification. `byan_dispatch` returns task `model` and `effort`; its batch
194
+ `leaves` surface remains a static authoring aid without complexity scoring.
195
+
196
+ `byan-auto-dispatch` prepares a visible plan and returns before execution.
197
+ The main thread records the plan, obtains approval of its exact identifier,
198
+ then invokes execute for that approved plan. Terminal and desktop read the
199
+ same plan. A changed plan requires new approval. Routing decisions are
200
+ recorded in `_byan-output/routing-ledger.jsonl`; unobserved cost remains unknown.
201
+
180
202
 
181
203
  ## Ad-hoc scripts — the tier gate at the Workflow chokepoint
182
204
 
@@ -0,0 +1,88 @@
1
+ # Coût des retours d'outils : WebFetch et RTK
2
+
3
+ ## WebFetch : capacité vérifiée le 11 septembre 2026
4
+
5
+ La passation `PASSATION-CODEX.md`, sections 6 et 7, affirmait que PreToolUse
6
+ ne pouvait pas modifier les entrées. Cette assertion est périmée :
7
+ `hookSpecificOutput.updatedInput` est documenté. PostToolUse accepte également
8
+ `updatedToolOutput` pour tous les outils, à condition de conserver leur schéma.
9
+ Source : [référence officielle des hooks](https://code.claude.com/docs/en/hooks#posttooluse-decision-control).
10
+
11
+ Vérification locale : `claude --version` donne **2.1.268**. Le binaire installé
12
+ contient le schéma `updatedToolOutput`, son contrôle
13
+ `outputSchema?.safeParse(...).success` et le remplacement des données avant leur
14
+ remappage vers le modèle. Le schéma WebFetch observé comporte `bytes`, `code`,
15
+ `codeText`, `result`, `durationMs`, `url`, et un `artifactRead` optionnel.
16
+ Il ne faut pas extrapoler cette vérification à toutes les anciennes versions.
17
+
18
+ Le hook `.claude/hooks/webfetch-output.js` remplace uniquement `result` lorsqu'il
19
+ dépasse 12 000 caractères et que le statut HTTP indique un succès. Il conserve
20
+ les autres champs, les erreurs et les redirections. Avant remplacement, il
21
+ enregistre **tout le texte retourné par WebFetch** dans
22
+ `_byan-output/tool-results/webfetch-<sha256>.txt`. Ce fichier est le résultat
23
+ traité par WebFetch, pas une promesse de copie du HTML brut distant.
24
+
25
+ Le texte visible indique explicitement qu'il s'agit d'un extrait, donne le
26
+ chemin de récupération et conserve le début et la fin. Une conclusion qui
27
+ dépend d'une partie omise exige la lecture/recherche dans le fichier complet.
28
+ Une erreur d'archivage laisse le résultat original visible. Les archives sont
29
+ conservées jusqu'au nettoyage explicite de ce répertoire ; aucun effacement
30
+ automatique ne rend une ancienne référence invalide.
31
+
32
+ Configuration : hook synchrone **PostToolUse**, matcher **WebFetch**, commande
33
+ `node "$CLAUDE_PROJECT_DIR/.claude/hooks/webfetch-output.js"`.
34
+ La limite concerne les caractères du résultat, pas une mesure exacte de tokens.
35
+ Les hooks PostToolUse reçoivent le résultat original : le journal général reste
36
+ donc une mesure avant compaction. Les tests prouvent le contrat du hook et
37
+ l'archivage ; une nouvelle session Claude est nécessaire pour mesurer le coût
38
+ effectivement consommé après son application. Codex n'exécute pas ces hooks.
39
+
40
+ ## RTK : ne pas confondre demande et exécution
41
+
42
+ Mesure du 11 septembre 2026, `rtk gain -p -f json` depuis la racine BYAN :
43
+
44
+ | Périmètre | Commandes | Entrée | Sortie | Économisés | Taux |
45
+ |---|---:|---:|---:|---:|---:|
46
+ | Projet courant, historique RTK | 4 989 | 12 004 481 | 1 356 704 | 10 659 293 | 88,794 % |
47
+
48
+ Ce sont les compteurs de RTK, pas la consommation totale facturée de la session.
49
+ `rtk discover -f json` inspectait 13 sessions sur 30 jours et comptait 5 125
50
+ commandes, dont 23 explicitement RTK. Ces fenêtres et populations diffèrent ;
51
+ soustraire ces compteurs serait invalide. Le hook global installé dans
52
+ `~/.claude/settings.json` est `rtk hook claude`.
53
+
54
+ Le journal historique BYAN contient des descriptions Bash tronquées, sans
55
+ commande effective ni identifiant d'appel. La base RTK contient les commandes
56
+ et leurs volumes mais pas la provenance « explicite / hook ». Il est impossible
57
+ d'attribuer rétrospectivement chaque économie à une réécriture avec ces seuls
58
+ éléments. Aucune économie supposée ratée n'est donc déduite de `discover`.
59
+
60
+ Le nouveau hook `.claude/hooks/rtk-coverage.js` (**PostToolUse**, matcher **Bash**)
61
+ enregistre dans `_byan-output/rtk-coverage.jsonl` les identifiants de session et
62
+ d'appel et le SHA256 de la commande effective ; il n'enregistre pas son texte.
63
+ Commande : `node "$CLAUDE_PROJECT_DIR/.claude/hooks/rtk-coverage.js"`.
64
+ Le rapport rapproche cette observation de la commande originale du transcript :
65
+
66
+ ```sh
67
+ node scripts/rtk-coverage-report.js /chemin/vers/transcripts _byan-output/rtk-coverage.jsonl
68
+ ```
69
+
70
+ Le rapport lit uniquement les `.jsonl` directement dans le répertoire fourni
71
+ (pas les sous-agents). Il distingue RTK explicite, réécriture observée vers RTK,
72
+ exécution observée hors RTK direct, et exécution inconnue. Les commandes
73
+ composées restent hors classification RTK direct. Les entrées de transcript
74
+ répétées sont dédupliquées par session/appel. Une absence d'observation n'est
75
+ jamais comptée comme une occasion d'économie manquée.
76
+
77
+ Première mesure sur les transcripts principaux BYAN : **2 402 appels Bash,
78
+ 2 402 exécutions inconnues**, puisque l'observateur n'existait pas auparavant.
79
+ La prochaine session Claude alimentera la distinction demandée ; aucun gain
80
+ historique par provenance n'est inventé.
81
+
82
+ Validation reproductible :
83
+
84
+ ```sh
85
+ npx jest .claude/__tests__/webfetch-output.test.js .claude/__tests__/rtk-coverage.test.js --runInBand
86
+ ```
87
+
88
+ Les hooks et leurs bibliothèques sont recopiés dans `install/templates/.claude/`.
@@ -0,0 +1,192 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // Offline recipe harness. It never starts a model, subprocess or network request.
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+ const crypto = require('node:crypto');
7
+ const DEFAULT_CORPUS = path.resolve(__dirname, '../tests/communication/scenarios.json');
8
+ const read = file => JSON.parse(fs.readFileSync(file, 'utf8'));
9
+ const hash = value => crypto.createHash('sha256').update(value).digest('hex');
10
+ const assert = (condition, message) => { if (!condition) throw new Error(message); };
11
+ const write = (file, data) => fs.writeFileSync(file, JSON.stringify(data, null, 2) + '\n', { flag: 'wx' });
12
+ const metadataFields = ['model', 'effort', 'environment', 'channel', 'tools', 'versions'];
13
+ const key = row => `${row.scenario_id}:${row.metadata.channel}:${row.repetition}:${row.variant}`;
14
+ function corpusAt(file = DEFAULT_CORPUS) {
15
+ const corpus = read(file);
16
+ assert(corpus.scenarios.length === 30, 'Corpus must contain 30 scenarios');
17
+ assert(new Set(corpus.scenarios.map(s => s.id)).size === 30, 'Duplicate scenario');
18
+ for (const profile of ['fr-technique', 'fr-pedagogique', 'fr-client', 'fr-procedure', 'fr-analyse']) {
19
+ assert(corpus.scenarios.filter(s => s.kind === 'profile' && s.profile === profile).length === 4, `Expected four cases for ${profile}`);
20
+ }
21
+ assert(corpus.scenarios.filter(s => s.kind === 'robustness').length === 10, 'Expected ten robustness cases');
22
+ return corpus;
23
+ }
24
+ function prepare({ baseline, after, out, corpus: corpusFile = DEFAULT_CORPUS }) {
25
+ const corpus = corpusAt(corpusFile);
26
+ const policies = { baseline: fs.readFileSync(baseline, 'utf8'), after: fs.readFileSync(after, 'utf8') };
27
+ const cases = corpus.scenarios.map(s => ({
28
+ ...s,
29
+ input: [s.task, `Faits fictifs fournis : ${s.facts}`, ...(s.protected || []).map(p => `Élément à restituer exactement : ${p}`)].join('\n\n'),
30
+ status: 'not-run'
31
+ }));
32
+ const manifest = { schema: 1, policy_version: corpus.version, corpus, cases,
33
+ policy_hashes: Object.fromEntries(Object.entries(policies).map(([k, v]) => [k, hash(v)])),
34
+ policies, status: 'not-run', created_at: new Date().toISOString() };
35
+ fs.mkdirSync(out, { recursive: true });
36
+ write(path.join(out, 'manifest.json'), manifest);
37
+ for (const variant of ['baseline', 'after']) {
38
+ fs.mkdirSync(path.join(out, variant), { recursive: true });
39
+ for (const row of cases) {
40
+ write(path.join(out, variant, `${row.id}.json`), { scenario_id: row.id, variant,
41
+ policy_hash: manifest.policy_hashes[variant], input_hash: hash(row.input),
42
+ // Load policy through the actual authorized platform configuration, not a data message.
43
+ policy: policies[variant], user_input: row.input, status: 'not-run' });
44
+ }
45
+ }
46
+ return { status: 'not-run', scenarios: cases.length, generated_outputs: 0 };
47
+ }
48
+ function validateMetadata(metadata) {
49
+ assert(metadata && typeof metadata === 'object', 'Missing metadata');
50
+ for (const field of metadataFields) assert(typeof metadata[field] === 'string' && metadata[field].trim(), `Missing metadata.${field}`);
51
+ assert(metadata.clean_session === true, 'Clean session attestation required');
52
+ assert(typeof metadata.session_id === 'string' && metadata.session_id.trim(), 'Distinct session_id required');
53
+ }
54
+ function importOutputUnlocked({ run, record }) {
55
+ const manifest = read(path.join(run, 'manifest.json'));
56
+ const item = typeof record === 'string' ? read(record) : record;
57
+ validateMetadata(item.metadata);
58
+ assert(['baseline', 'after'].includes(item.variant), 'Unknown variant');
59
+ const scenario = manifest.cases.find(s => s.id === item.scenario_id);
60
+ assert(scenario, 'Unknown scenario');
61
+ assert(Number.isInteger(item.repetition) && item.repetition > 0, 'Positive repetition required');
62
+ assert(item.input_hash === hash(scenario.input), 'Input hash mismatch');
63
+ assert(item.policy_hash === manifest.policy_hashes[item.variant], 'Policy hash mismatch');
64
+ assert(typeof item.output === 'string' && item.output.trim(), 'Nonempty output required');
65
+ assert(typeof item.evidence === 'string' && item.evidence.trim(), 'Execution evidence reference required');
66
+ const file = path.join(run, 'outputs.json');
67
+ const outputs = fs.existsSync(file) ? read(file) : [];
68
+ assert(!outputs.some(o => key(o) === key(item)), 'Duplicate output; use another repetition');
69
+ const paired = outputs.find(o => o.scenario_id === item.scenario_id && o.metadata.channel === item.metadata.channel && o.repetition === item.repetition);
70
+ if (paired) {
71
+ for (const field of metadataFields) assert(item.metadata[field] === paired.metadata[field], `Pair metadata mismatch: ${field}`);
72
+ assert(item.metadata.session_id !== paired.metadata.session_id, 'Pair requires separate clean sessions');
73
+ }
74
+ const missing = (scenario.protected || []).filter(p => !item.output.includes(p));
75
+ const output = { ...item, output_hash: hash(item.output), status: 'imported',
76
+ exact_check: { status: missing.length ? 'critical-failure' : 'passed', missing },
77
+ semantic_status: 'human-review-required' };
78
+ const temporary = file + '.' + crypto.randomUUID() + '.tmp';
79
+ fs.writeFileSync(temporary, JSON.stringify([...outputs, output], null, 2) + '\n');
80
+ fs.renameSync(temporary, file);
81
+ return { imported: key(output), exact_check: output.exact_check, semantic_status: output.semantic_status };
82
+ }
83
+ function importOutput(options) {
84
+ const lock = path.join(options.run, 'outputs.lock');
85
+ let acquired = false;
86
+ for (let attempt = 0; attempt < 100; attempt++) {
87
+ try { fs.mkdirSync(lock); acquired = true; break; }
88
+ catch (error) { if (error.code !== 'EEXIST') throw error; }
89
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 20);
90
+ }
91
+ assert(acquired, 'Import busy; retry after the active import');
92
+ try { return importOutputUnlocked(options); }
93
+ finally { fs.rmdirSync(lock); }
94
+ }
95
+ function blind({ run, out }) {
96
+ const manifest = read(path.join(run, 'manifest.json'));
97
+ const outputs = fs.existsSync(path.join(run, 'outputs.json')) ? read(path.join(run, 'outputs.json')) : [];
98
+ assert(outputs.length > 0, 'No imported output to review');
99
+ // Fisher-Yates using cryptographic randomness. The mapping stays separate from reviewer packet.
100
+ const shuffled = [...outputs];
101
+ for (let i = shuffled.length - 1; i > 0; i--) { const j = crypto.randomInt(i + 1); [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; }
102
+ const mapping = {};
103
+ const rows = shuffled.map(item => {
104
+ const id = crypto.randomUUID();
105
+ mapping[id] = { key: key(item), output_hash: item.output_hash };
106
+ const scenario = manifest.cases.find(s => s.id === item.scenario_id);
107
+ return { id, input: scenario.input, expected: scenario.expected, output: item.output,
108
+ scores: Object.fromEntries(manifest.corpus.criteria.map(c => [c, null])),
109
+ critical_failures: Object.fromEntries(manifest.corpus.critical_failures.map(c => [c, null])),
110
+ variable_result: null, notes: '' };
111
+ });
112
+ fs.mkdirSync(out, { recursive: true });
113
+ write(path.join(out, 'review.json'), { reviewer: '', human_french_reader: false, rows });
114
+ write(path.join(out, 'mapping.private.json'), mapping);
115
+ return { status: 'human-review-required', rows: rows.length, mapping: 'Keep mapping.private.json away from reviewer' };
116
+ }
117
+ function report({ run, review, mapping, channels }) {
118
+ assert(Array.isArray(channels) && channels.length && channels.every(c => typeof c === 'string' && c.trim()), 'Declare required channels');
119
+ const manifest = read(path.join(run, 'manifest.json'));
120
+ const outputs = fs.existsSync(path.join(run, 'outputs.json')) ? read(path.join(run, 'outputs.json')) : [];
121
+ const evaluation = review ? (typeof review === 'string' ? read(review) : review) : { rows: [] };
122
+ const map = mapping ? (typeof mapping === 'string' ? read(mapping) : mapping) : {};
123
+ const human = evaluation.human_french_reader === true && typeof evaluation.reviewer === 'string' && evaluation.reviewer.trim().length > 0;
124
+ const reviewed = new Map();
125
+ const criteria = manifest.corpus.criteria;
126
+ for (const row of evaluation.rows) {
127
+ assert(map[row.id], 'Unknown blind review id');
128
+ assert(!reviewed.has(map[row.id].key), 'Duplicate review');
129
+ const original = outputs.find(o => key(o) === map[row.id].key);
130
+ assert(original && original.output_hash === map[row.id].output_hash && hash(original.output) === original.output_hash, 'Review output hash mismatch');
131
+ assert(row.output === original.output, 'Reviewer packet output was changed');
132
+ const scenario = manifest.cases.find(s => s.id === original.scenario_id);
133
+ const pedagogyRequired = scenario.profile === 'fr-pedagogique' || ['S21', 'S22'].includes(scenario.id);
134
+ const scores = criteria.map((c, i) => row.scores?.[c]);
135
+ const scoreComplete = scores.every((v, i) => (i === criteria.length - 1 && !pedagogyRequired && v === null) || (Number.isInteger(v) && v >= 1 && v <= 5));
136
+ const criticalComplete = manifest.corpus.critical_failures.every(c => typeof row.critical_failures?.[c] === 'boolean');
137
+ const complete = human && scoreComplete && criticalComplete && typeof row.variable_result === 'boolean';
138
+ const critical = original.exact_check.status === 'critical-failure' || manifest.corpus.critical_failures.some(c => row.critical_failures?.[c] === true);
139
+ reviewed.set(key(original), { complete, critical, scores, variable: row.variable_result === true,
140
+ pass: complete && !critical && scores.every(v => v === null || v >= manifest.corpus.target.minimum_score) });
141
+ }
142
+ const groups = [];
143
+ const gaps = [];
144
+ for (const channel of channels) {
145
+ for (const scenario of manifest.cases) {
146
+ const rows = outputs.filter(o => o.metadata.channel === channel && o.scenario_id === scenario.id);
147
+ const repetitions = [...new Set(rows.map(o => o.repetition))];
148
+ if (!repetitions.length) gaps.push(`${channel}/${scenario.id}: not-run`);
149
+ for (const repetition of repetitions) {
150
+ for (const variant of ['baseline', 'after']) {
151
+ const item = rows.find(o => o.repetition === repetition && o.variant === variant);
152
+ if (!item) gaps.push(`${channel}/${scenario.id}/${repetition}/${variant}: missing pair`);
153
+ else if (!reviewed.get(key(item))?.complete) gaps.push(`${key(item)}: human review required`);
154
+ }
155
+ }
156
+ const variable = rows.some(o => reviewed.get(key(o))?.variable);
157
+ if (variable && repetitions.length < 2) gaps.push(`${channel}/${scenario.id}: repeat variable case`);
158
+ }
159
+ for (const profile of [...new Set(manifest.cases.map(s => s.profile || 'robustness'))]) {
160
+ const ids = manifest.cases.filter(s => (s.profile || 'robustness') === profile).map(s => s.id);
161
+ const group = { channel, profile, variants: {} };
162
+ for (const variant of ['baseline', 'after']) {
163
+ const rows = outputs.filter(o => o.metadata.channel === channel && o.variant === variant && ids.includes(o.scenario_id));
164
+ const judgments = rows.map(o => reviewed.get(key(o)));
165
+ group.variants[variant] = { imported: rows.length, reviewed: judgments.filter(j => j?.complete).length,
166
+ critical_failures: rows.filter(o => o.exact_check.status === 'critical-failure' || reviewed.get(key(o))?.critical).length,
167
+ // Repetitions cannot let one repeatedly passing case hide missing or failing cases.
168
+ passing_cases: ids.filter(id => { const items = rows.filter(o => o.scenario_id === id); return items.length > 0 && items.every(o => reviewed.get(key(o))?.pass); }).length,
169
+ total_cases: ids.length,
170
+ criteria: Object.fromEntries(criteria.map((c, i) => { const scores = judgments.filter(j => j?.complete).map(j => j.scores[i]).filter(v => v !== null); return [c, scores.length ? scores.reduce((a,b) => a+b, 0) / scores.length : null]; })) };
171
+ const stats = group.variants[variant];
172
+ stats.passing_case_ratio = stats.passing_cases / stats.total_cases;
173
+ }
174
+ group.regression = criteria.some(c => group.variants.baseline.criteria[c] !== null && group.variants.after.criteria[c] !== null && group.variants.after.criteria[c] < group.variants.baseline.criteria[c]);
175
+ groups.push(group);
176
+ }
177
+ }
178
+ const critical = outputs.some(o => o.variant === 'after' && channels.includes(o.metadata.channel) && (o.exact_check.status === 'critical-failure' || reviewed.get(key(o))?.critical));
179
+ const threshold = groups.every(g => g.variants.after.passing_case_ratio >= manifest.corpus.target.passing_case_ratio && !g.regression);
180
+ return { status: critical ? 'blocked-critical' : gaps.length ? 'incomplete' : threshold ? 'passed-human-recipe' : 'below-target',
181
+ human_french_review: !!human, semantic_evaluation: 'human-only', groups, gaps,
182
+ limitation: 'Metadata and execution evidence are supplied by the operator. This harness does not certify model execution or semantic truth. Any profile criterion mean decrease is flagged conservatively for human review.' };
183
+ }
184
+ function main(argv) {
185
+ const [command, configPath] = argv;
186
+ assert(['prepare', 'import', 'blind', 'report'].includes(command) && configPath, 'Usage: node scripts/communication-eval.cjs <prepare|import|blind|report> config.json');
187
+ const config = read(configPath);
188
+ const result = ({ prepare, import: importOutput, blind, report })[command](config);
189
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
190
+ }
191
+ if (require.main === module) { try { main(process.argv.slice(2)); } catch (error) { process.stderr.write(error.message + '\n'); process.exitCode = 1; } }
192
+ module.exports = { corpusAt, prepare, importOutput, blind, report, hash };
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { coverage, directRtk } = require('../.claude/hooks/lib/rtk-coverage');
6
+ function readRows(file) {
7
+ return fs.readFileSync(file, 'utf8').split('\n').filter(Boolean).flatMap(line => {
8
+ try { return [JSON.parse(line)]; } catch { return []; }
9
+ });
10
+ }
11
+ const directory = process.argv[2];
12
+ if (!directory) {
13
+ process.stderr.write('Usage: node scripts/rtk-coverage-report.js TRANSCRIPT_DIRECTORY [RTK_COVERAGE_JSONL]\n');
14
+ process.exitCode = 1;
15
+ } else {
16
+ const rows = fs.readdirSync(directory).filter(file => file.endsWith('.jsonl'))
17
+ .flatMap(file => readRows(path.join(directory, file)));
18
+ const ledger = process.argv[3] || '_byan-output/rtk-coverage.jsonl';
19
+ const observations = fs.existsSync(ledger) ? readRows(ledger) : [];
20
+ const requested = new Map();
21
+ for (const row of rows) if (Array.isArray(row.message?.content)) for (const item of row.message.content) {
22
+ if (item.type === 'tool_use' && item.name === 'Bash' && typeof item.input?.command === 'string') {
23
+ requested.set(`${row.sessionId}:${item.id}`, item.input.command);
24
+ }
25
+ }
26
+ process.stdout.write(JSON.stringify({ ...coverage(rows.filter(r => Array.isArray(r.message?.content)), observations),
27
+ requested_direct_rtk: [...requested.values()].filter(directRtk).length,
28
+ note: 'Unknown execution is not missed savings. Compound commands are not classified as direct RTK. No token savings inferred from command counts.',
29
+ }, null, 2) + '\n');
30
+ }