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.
- package/CHANGELOG.md +17 -0
- package/README.md +10 -0
- package/install/bin/byan-communication.cjs +11 -0
- package/install/lib/communication-manifest.json +243 -0
- package/install/lib/communication-migration.cjs +91 -0
- package/install/lib/communication-previous.json +181 -0
- package/install/lib/gitignore.js +135 -0
- package/install/lib/install-engine.js +17 -1
- package/install/lib/ownership.js +24 -5
- package/install/package.json +1 -1
- package/install/templates/.claude/agents/bmad-byan.md +13 -26
- package/install/templates/.claude/agents/bmad-tao.md +47 -0
- package/install/templates/.claude/hooks/auto-dispatch-gate.js +81 -0
- package/install/templates/.claude/hooks/inject-tao.js +15 -71
- package/install/templates/.claude/hooks/inject-voice-anchor.js +21 -154
- package/install/templates/.claude/hooks/lib/dispatch-approval.cjs +135 -0
- package/install/templates/.claude/hooks/lib/dispatch-plan-format.js +293 -0
- package/install/templates/.claude/hooks/lib/plain-language.js +7 -14
- package/install/templates/.claude/hooks/lib/rtk-coverage.js +31 -0
- package/install/templates/.claude/hooks/lib/tool-log.js +143 -0
- package/install/templates/.claude/hooks/lib/voice-conformance.js +9 -32
- package/install/templates/.claude/hooks/lib/webfetch-output.js +37 -0
- package/install/templates/.claude/hooks/rtk-coverage.js +18 -0
- package/install/templates/.claude/hooks/tool-failure-guard.js +16 -9
- package/install/templates/.claude/hooks/webfetch-output.js +17 -0
- package/install/templates/.claude/rules/native-workflows.md +24 -5
- package/install/templates/.claude/rules/plain-language.md +7 -86
- package/install/templates/.claude/settings.json +56 -0
- package/install/templates/.claude/skills/byan-byan/SKILL.md +42 -83
- package/install/templates/.claude/skills/byan-hermes-dispatch/SKILL.md +22 -7
- package/install/templates/.claude/workflows/byan-auto-dispatch.js +86 -38
- package/install/templates/.codex/skills/byan/SKILL.md +7 -0
- package/install/templates/AGENTS.md +106 -0
- package/install/templates/_byan/_config/communication-policy.json +50 -0
- package/install/templates/_byan/_config/workflow-manifest.csv +2 -0
- package/install/templates/_byan/agent/byan/byan-soul.md +14 -2
- package/install/templates/_byan/agent/byan/byan-tao.md +35 -274
- package/install/templates/_byan/agent/byan/byan.md +6 -2
- package/install/templates/_byan/agent/byan/soul.md +419 -0
- package/install/templates/_byan/agent/byan/tao.md +60 -0
- package/install/templates/_byan/agent/tao/tao.md +26 -27
- package/install/templates/_byan/connaissance/mantras-sources.md +295 -0
- package/install/templates/_byan/core/activation/soul-activation.md +6 -5
- package/install/templates/_byan/core/communication.cjs +76 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/agent-matcher.js +38 -1
- package/install/templates/_byan/mcp/byan-mcp-server/lib/communication.cjs +77 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/dispatch-router.js +12 -3
- package/install/templates/_byan/mcp/byan-mcp-server/lib/dispatch.js +51 -20
- package/install/templates/_byan/mcp/byan-mcp-server/lib/fd-ack.js +75 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/leantime-fd-core.js +12 -1
- package/install/templates/_byan/mcp/byan-mcp-server/lib/native-tiers.js +114 -2
- package/install/templates/_byan/mcp/byan-mcp-server/lib/routing-ledger.js +356 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/soul.js +9 -0
- package/install/templates/_byan/mcp/byan-mcp-server/server.js +24 -6
- package/install/templates/_byan/mcp/byan-mcp-server/skill-bundles-manifest.json +2 -2
- package/install/templates/_byan/workflow/simple/byan/data/mantras.yaml +2 -5
- package/install/templates/_byan/workflow/simple/byan/templates/tao-template.md +56 -0
- package/install/templates/docs/auto-dispatch.md +76 -0
- package/install/templates/docs/communication.md +69 -0
- package/install/templates/docs/native-workflows-contract.md +33 -11
- package/install/templates/docs/tool-output-cost.md +88 -0
- package/install/templates/scripts/communication-eval.cjs +192 -0
- package/install/templates/scripts/rtk-coverage-report.js +30 -0
- package/install/templates/src/byan-v2/data/mantras.json +1058 -0
- package/install/templates/tests/communication/README.md +106 -0
- package/install/templates/tests/communication/incises.test.cjs +26 -0
- package/install/templates/tests/communication/scenarios.json +391 -0
- package/package.json +1 -1
- 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) |
|
|
100
|
-
| `balanced` | `sonnet` | MECHANICAL
|
|
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
|
-
|
|
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
|
-
- **
|
|
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").
|
|
117
|
-
|
|
118
|
-
- **`mech-` is a held declaration.**
|
|
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.
|
|
178
|
-
|
|
179
|
-
|
|
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
|
+
}
|