create-byan-agent 2.44.0 → 2.47.0

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 (24) hide show
  1. package/CHANGELOG.md +95 -0
  2. package/install/templates/.claude/CLAUDE.md +11 -0
  3. package/install/templates/.claude/hooks/agent-gate-check.js +53 -0
  4. package/install/templates/.claude/hooks/inject-voice-anchor.js +34 -1
  5. package/install/templates/.claude/hooks/lib/agent-gate.js +127 -0
  6. package/install/templates/.claude/hooks/lib/plain-language.js +155 -0
  7. package/install/templates/.claude/hooks/plain-language-check.js +53 -0
  8. package/install/templates/.claude/rules/agent-entry-gate.md +83 -0
  9. package/install/templates/.claude/rules/plain-language.md +73 -0
  10. package/install/templates/.claude/settings.json +8 -0
  11. package/install/templates/.claude/skills/byan-byan/SKILL.md +14 -0
  12. package/install/templates/.claude/workflows/intelligent-dispatch.js +68 -0
  13. package/install/templates/_byan/agent/byan/byan-tao.md +12 -0
  14. package/install/templates/_byan/mcp/byan-mcp-server/lib/agent-matcher.js +167 -0
  15. package/install/templates/_byan/mcp/byan-mcp-server/lib/codex-bridge.js +176 -0
  16. package/install/templates/_byan/mcp/byan-mcp-server/lib/dispatch-blackboard.js +114 -0
  17. package/install/templates/_byan/mcp/byan-mcp-server/lib/dispatch-orchestrator.js +116 -0
  18. package/install/templates/_byan/mcp/byan-mcp-server/lib/dispatch-router.js +149 -0
  19. package/install/templates/_byan/mcp/byan-mcp-server/server.js +37 -0
  20. package/install/templates/_byan/mcp/byan-mcp-server/skill-bundles-manifest.json +1 -1
  21. package/install/templates/_byan/workflow/simple/byan/data/mantras.yaml +15 -4
  22. package/install/templates/dist/skill-bundles/byan-byan.zip +0 -0
  23. package/install/templates/docs/intelligent-dispatch.md +84 -0
  24. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -9,6 +9,101 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [2.47.0] - 2026-07-16
13
+
14
+ ### Added - Mandatory agent entry gate (match-or-create), the base of BYAN
15
+
16
+ - Makes the agent dispatch the front door of every non-conversational task: BYAN +
17
+ Hermes evaluate which specialist agent fits the need and PROPOSE it ; the user
18
+ validates (double validation IA + human) ; THEN the workflow runs. No suited
19
+ agent -> propose an interview to frame the need -> web research on the trade's
20
+ competencies + best practices -> create the tailored agent -> workflow. The
21
+ interview is triggered by the ABSENCE of a suited agent, not by task size (a fit
22
+ routes directly, no ceremony). This closes the gap where a plain task got done
23
+ inline without any dispatch.
24
+ - F1 `lib/agent-matcher.js` — pure suitability pre-filter: (task text + roster)
25
+ -> ranked candidates + a {fit | no-fit} verdict. Curated Hermes trigger words
26
+ (weighted) over a title/role text overlap ; accent-insensitive ; a no-fit is the
27
+ interview signal. Loader reads the roster from agent-manifest.csv (robust CSV
28
+ parse). It PROPOSES, it does not decide alone.
29
+ - F2/F3 doctrine `.claude/rules/agent-entry-gate.md` (+ pointer in CLAUDE.md + a
30
+ section 0 in the byan-byan skill): the rail, the two dispatch layers (agent vs
31
+ runtime), the proportionality (verify every task, interview only on no-fit), the
32
+ double validation, and the web-research step in agent creation.
33
+ - F4 reactive net `.claude/hooks/agent-gate-check.js` (+ pure core
34
+ `lib/agent-gate.js`): a Stop hook that flags a task done directly (files written)
35
+ with no agent proposal and outside an active FD cycle ; the next-turn voice
36
+ reminder surfaces it in plain French. Non-blocking (the correction lands next
37
+ turn) ; exits 0 in every path. Honest ceiling: no pre-display interception, so
38
+ the rail is doctrine + this net, not a first-turn wall.
39
+ - F5: tests (`test/agent-matcher.test.js` node:test, `.claude/__tests__/agent-gate.test.js`
40
+ jest). Full MCP suite + jest suite green ; workflow linter OK ; skill bundles
41
+ re-checked ; zero emoji. Shipped via install/templates (5 files added to the sync
42
+ manifest). Doc: `.claude/rules/agent-entry-gate.md`.
43
+
44
+ ## [2.46.0] - 2026-07-15
45
+
46
+ ### Added - Intelligent dispatch: Codex/Claude routing + architect-dev loop (option B)
47
+
48
+ - Route each task to the runtime that is genuinely better for it, on the right
49
+ model and effort for its complexity, and let an architect (Claude) and a dev
50
+ (Codex or Claude) exchange turn by turn until the work converges. Layered,
51
+ ports-and-adapters design so the durable core stays independent of the transport.
52
+ - F1 `lib/dispatch-router.js` — pure routing brain: {nature, complexity} ->
53
+ {runtime, model, effort}. Runtime table is the cross-checked result (Codex for
54
+ execution/shell/deploy/devops/browser ; Claude for architecture/refactor/quality/
55
+ planning ; unknown -> Claude). Complexity picks the Claude model tier via
56
+ native-tiers (haiku/sonnet/inherit) or the Codex reasoning-effort (low/med/high).
57
+ Two red lines enforced in code, not left to the caller: Fable is refused
58
+ (assertNoFable throws) and verification is forced to Claude (a runtime does not
59
+ grade its own work).
60
+ - F2 `lib/codex-bridge.js` — Codex transport behind a swappable adapter. The
61
+ shipped codex-exec adapter runs `codex exec` read-only for a unified diff that
62
+ Claude applies with git apply (Codex writes nothing -> works under a locked-down
63
+ sandbox e.g. Landlock). Failure is a value (ok:false + reason) so the loop falls
64
+ back to Claude. codex-mcp is a declared V2 slot (available:false) for the tighter
65
+ `codex mcp-server` coupling later, with no change to F1/F4.
66
+ - F3 `lib/dispatch-blackboard.js` — the turn-by-turn shared board the agents use to
67
+ exchange (build/render pure ; JSONL sidecar I/O isolated). The honest substitute
68
+ for a live peer chat: an orchestrated loop, like a chat server holding the turns.
69
+ - F4 `lib/dispatch-orchestrator.js` — the loop core: routes via F1, runs
70
+ architect<->dev through the board until convergence, executors injected (Codex vs
71
+ Claude) so it is fully unit-tested. `.claude/workflows/intelligent-dispatch.js` is
72
+ the native launch facade for one routed task (design -> implement -> verify ;
73
+ the verify leaf is a Claude reviewer, not the dev).
74
+ - Enforcement: the Codex auto-delegate config ships DISARMED (absent = off) ;
75
+ arming stays a per-machine opt-in (`_byan/_config/autodelegate.json`, gitignored).
76
+ Unit tests: `test/dispatch-router|codex-bridge|dispatch-blackboard|dispatch-orchestrator.test.js`.
77
+ Also corrected a pre-existing api-tools test drift (byan_api_knowledge_retrieve
78
+ was registered in server.js but absent from the test's tool list). Full MCP suite
79
+ green (833 node:test) + jest suite green. Doc: `docs/intelligent-dispatch.md`.
80
+ Shipped via install/templates (10 files added to the sync manifest).
81
+
82
+ ## [2.45.0] - 2026-07-15
83
+
84
+ ### Added - Plain-language guard for every agent (Mantra IA-26 "Parler Reel")
85
+
86
+ - New team-wide rule: agents speak clear, coherent French to the user — no gratuitous
87
+ English when a French word exists ("redemarrer le conteneur", not "cutoff"), no raw
88
+ internal jargon (leaf/tier/downgrade/gate/inline/advisory), no misapplied metaphor
89
+ (you do not "forge" a token). A technical term with no French equivalent (commit,
90
+ cache, token) is kept but explained once. Test: the reader understands with no
91
+ dictionary. This is the sibling of IA-23 (no emoji) and applies to ALL agents.
92
+ - Mechanism, in three layers, none of which re-generates an already-shown reply:
93
+ 1. The rule reaches every agent: mantra `IA-26` (`mantras.yaml` + `mantras-sources.md`),
94
+ `.claude/rules/plain-language.md`, and a pointer in `CLAUDE.md`.
95
+ 2. BYAN's own voice stays fresh: a line in the per-turn reminder
96
+ (`inject-voice-anchor.js`) + entries in `tao.md` Section 4 (Vocabulaire Interdit).
97
+ 3. A forward net (non-blocking): the Stop hook `plain-language-check.js` spots the
98
+ known repeat-offenders in the finished reply and writes a one-turn flag under
99
+ `_byan-output/`; the next-turn reminder reads it, signals it in plain French, and
100
+ clears it. No blocking, no re-answer — the correction is carried to the next turn
101
+ (a blocking guard would force a costly regen and the user has already read the slip).
102
+ - Core logic isolated in `.claude/hooks/lib/plain-language.js` (French-aware word
103
+ boundary so "metier"/"chantier" stay clear of the "tier" match; code spans stripped
104
+ before scanning). Tests: `.claude/__tests__/plain-language.test.js`. Full suite green
105
+ (2629 jest). Shipped via `install/templates/`.
106
+
12
107
  ## [2.44.0] - 2026-07-15
13
108
 
14
109
  ### Changed - Revived the Sonnet middle tier for native-workflow model routing
@@ -15,6 +15,16 @@ Pour invoquer Hermes, tape: `@hermes` ou demande simplement "quel agent pour [ta
15
15
 
16
16
  Voir @.claude/rules/hermes-dispatcher.md pour les commandes Hermes.
17
17
 
18
+ ## Porte d'entree — dispatch d'agent obligatoire (match-or-create)
19
+
20
+ La base de BYAN : toute tache non-conversationnelle passe d'abord par le dispatch
21
+ d'agent. BYAN + Hermes evaluent quel agent specialiste colle au besoin et le
22
+ PROPOSENT ; l'utilisateur valide (double validation IA + humain), PUIS on lance le
23
+ workflow. Aucun agent adapte -> interview pour cadrer le besoin -> recherche web
24
+ (competences + bonnes pratiques du metier) -> creation de l'agent sur mesure ->
25
+ workflow. Le declencheur de l'interview est l'absence d'un agent adapte, pas la
26
+ taille de la tache. Detail + enforcement : voir @.claude/rules/agent-entry-gate.md
27
+
18
28
  ## Architecture BYAN
19
29
 
20
30
  ```
@@ -33,6 +43,7 @@ Voir @.claude/rules/hermes-dispatcher.md pour les commandes Hermes.
33
43
 
34
44
  - Pas d'emojis dans le code, commits, ou specs techniques (Mantra IA-23)
35
45
  - Code auto-documente, commentaires uniquement pour le POURQUOI (Mantra IA-24)
46
+ - Parler reel: francais clair et coherent avec l'utilisateur, zero jargon interne / anglais gratuit (Mantra IA-26, voir @.claude/rules/plain-language.md)
36
47
  - Format commits: `type: description` (feat, fix, docs, refactor, test, chore)
37
48
  - Simplicite d'abord - Rasoir d'Ockham (Mantra #37)
38
49
  - Challenge Before Confirm - Valider avant d'accepter (Mantra IA-16)
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // Stop hook — the reactive net for BYAN's mandatory agent entry gate (F4).
5
+ //
6
+ // At end of turn it asks: was a task handled DIRECTLY (files written) without any
7
+ // agent proposal and outside an active FD cycle ? If so it writes a one-turn flag
8
+ // under _byan-output/ ; the next-turn voice reminder surfaces it in plain French.
9
+ // It NEVER blocks the turn (the user asked to signal, not trap) — always exits 0.
10
+ //
11
+ // The judgment lives in lib/agent-gate.js (pure) ; this shell only extracts the
12
+ // signals from the transcript + fd-state.
13
+
14
+ const fs = require('fs');
15
+ const { extractLastAssistantText, extractRecentMessages } = require('./lib/transcript-read');
16
+ const gate = require('./lib/agent-gate');
17
+
18
+ // detect(payload, projectDir) — testable core. Returns the assessment and writes
19
+ // the flag on a slip.
20
+ function detect(payload, projectDir) {
21
+ const text = extractLastAssistantText(payload);
22
+ const messages = extractRecentMessages(payload, 8) || [];
23
+ const assessment = gate.assessTurn({
24
+ wroteFiles: gate.hasWriteActivity(messages),
25
+ proposedAgent: gate.hasProposalMarker(text),
26
+ fdActive: gate.fdIsActive(projectDir),
27
+ });
28
+ if (assessment.slip) gate.writeSlip(projectDir, assessment.reason);
29
+ return assessment;
30
+ }
31
+
32
+ function readStdin() {
33
+ try { return fs.readFileSync(0, 'utf8'); } catch { return ''; }
34
+ }
35
+
36
+ if (require.main === module) {
37
+ const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
38
+ let payload = {};
39
+ try {
40
+ const raw = readStdin();
41
+ if (raw && raw.trim()) payload = JSON.parse(raw);
42
+ } catch {
43
+ payload = {};
44
+ }
45
+ try {
46
+ detect(payload, projectDir);
47
+ } catch {
48
+ // never block end-of-turn
49
+ }
50
+ process.stdout.write('{}');
51
+ }
52
+
53
+ module.exports = { detect };
@@ -26,6 +26,8 @@
26
26
  const fs = require('fs');
27
27
  const path = require('path');
28
28
  const { buildTaoContext, turnCounterPath } = require('./inject-tao');
29
+ const pl = require('./lib/plain-language');
30
+ const gate = require('./lib/agent-gate');
29
31
 
30
32
  const ANCHOR = [
31
33
  'Voix BYAN (rappel par tour ; tao complet chargé au démarrage de session) :',
@@ -33,6 +35,7 @@ const ANCHOR = [
33
35
  '- Challenge avant de confirmer ; questionne les absolus (Mantra IA-16).',
34
36
  '- Signatures : "Attends — pourquoi ?", "OK. On construit.", "Ça, c\'est du générique.".',
35
37
  '- Zéro emoji. Orienté solution : on cherche la meilleure option, pas le mur.',
38
+ '- Français réel et cohérent (Mantra IA-26) : pas d\'anglais gratuit (dis "redémarrer le conteneur", pas "cutoff"), pas de jargon interne brut, pas de métaphore collée de travers ("forger" un token).',
36
39
  ].join('\n');
37
40
 
38
41
  const DEFAULT_REFRESH_EVERY = 12;
@@ -77,6 +80,22 @@ function decideAnchor({ turn, every, fullTao }) {
77
80
  return { mode: 'anchor', additionalContext: ANCHOR };
78
81
  }
79
82
 
83
+ // Append a plain-language slip reminder (IA-25) to the injected context when the
84
+ // previous turn tripped the forward net. Pure so it is unit-testable; the fs read
85
+ // + clear stays in the require.main path below. A missing/empty hit list is a
86
+ // no-op, so this never changes the anchor on a clean turn.
87
+ function withSlipReminder(baseContext, hits) {
88
+ const reminder = pl.formatReminder(hits);
89
+ return reminder ? `${baseContext}\n${reminder}` : baseContext;
90
+ }
91
+
92
+ // Append the agent-gate reminder (F4) when the previous turn did a task directly
93
+ // without proposing an agent. Pure; the fs read + clear stays in require.main.
94
+ function withGateReminder(baseContext, slip) {
95
+ const reminder = gate.formatReminder(slip);
96
+ return reminder ? `${baseContext}\n${reminder}` : baseContext;
97
+ }
98
+
80
99
  if (require.main === module) {
81
100
  const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
82
101
  const every = refreshEvery();
@@ -84,9 +103,21 @@ if (require.main === module) {
84
103
  writeTurn(projectDir, turn);
85
104
  const fullTao = every > 0 && turn % every === 0 ? buildTaoContext(projectDir) : '';
86
105
  const { additionalContext } = decideAnchor({ turn, every, fullTao });
106
+ // Forward net: if the previous turn slipped into jargon, remind now and clear
107
+ // the flag (one-shot). Read/clear here, formatting stays pure in withSlipReminder.
108
+ const slipHits = pl.readSlip(projectDir);
109
+ if (slipHits && slipHits.length) pl.clearSlip(projectDir);
110
+ // Forward net #2 (F4): agent entry-gate slip. Read + clear one-shot.
111
+ const gateSlip = gate.readSlip(projectDir);
112
+ if (gateSlip) gate.clearSlip(projectDir);
113
+ let ctx = withSlipReminder(additionalContext, slipHits);
114
+ ctx = withGateReminder(ctx, gateSlip);
87
115
  process.stdout.write(
88
116
  JSON.stringify({
89
- hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext },
117
+ hookSpecificOutput: {
118
+ hookEventName: 'UserPromptSubmit',
119
+ additionalContext: ctx,
120
+ },
90
121
  })
91
122
  );
92
123
  }
@@ -98,5 +129,7 @@ module.exports = {
98
129
  readTurn,
99
130
  writeTurn,
100
131
  decideAnchor,
132
+ withSlipReminder,
133
+ withGateReminder,
101
134
  DEFAULT_REFRESH_EVERY,
102
135
  };
@@ -0,0 +1,127 @@
1
+ 'use strict';
2
+
3
+ // F4 core — the reactive net for the mandatory agent entry gate.
4
+ //
5
+ // BYAN's philosophy: every non-conversational task passes through Hermes+BYAN
6
+ // PROPOSING a suited agent (or an interview to create one), validated by the
7
+ // user, BEFORE any work. No pre-display hook exists, so this net is post-hoc: at
8
+ // end of turn it detects a task done DIRECTLY (files written) without any agent
9
+ // proposal and without an active FD cycle, writes a one-turn flag, and the
10
+ // next-turn reminder surfaces it. Non-blocking by design (the user asked to
11
+ // "signal", not to trap the turn) — the correction lands on the next turn.
12
+ //
13
+ // Pure: assessTurn + the marker/write detectors take plain inputs. The flag I/O
14
+ // is isolated. The hook shell feeds it transcript + fd-state signals.
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+
19
+ // Text signals that the entry gate WAS engaged this turn (an agent was proposed,
20
+ // an FD phase is being narrated, a specialist was addressed, or the interview /
21
+ // no-fit path was surfaced). Any of these means "not a silent direct-do".
22
+ const PROPOSAL_MARKERS = [
23
+ '[fd:', '@hermes', 'agent adapte', 'aucun agent', 'je propose', 'proposition d\'agent',
24
+ 'interview', 'byan-hermes-dispatch', 'dispatch d\'agent', 'quel agent',
25
+ ];
26
+
27
+ // Tool names that count as "did real work on the repo" this turn.
28
+ const WRITE_TOOLS = new Set(['Write', 'Edit', 'MultiEdit', 'NotebookEdit']);
29
+
30
+ function hasProposalMarker(text) {
31
+ const t = String(text == null ? '' : text).toLowerCase();
32
+ return PROPOSAL_MARKERS.some((m) => t.includes(m));
33
+ }
34
+
35
+ // hasWriteActivity(messages) — true if any recent assistant message carried a
36
+ // tool_use block that writes to the repo. `messages` is an array of
37
+ // { role, content } where content is a string or a block array.
38
+ function hasWriteActivity(messages) {
39
+ if (!Array.isArray(messages)) return false;
40
+ for (const m of messages) {
41
+ if (!m || m.role !== 'assistant' || !Array.isArray(m.content)) continue;
42
+ for (const block of m.content) {
43
+ if (block && block.type === 'tool_use' && WRITE_TOOLS.has(block.name)) return true;
44
+ }
45
+ }
46
+ return false;
47
+ }
48
+
49
+ // assessTurn — the decision. A slip is a task done directly (files written) with
50
+ // no agent proposal AND no active FD cycle. When an FD is engaged, the gate is
51
+ // already in play (DISPATCH is part of it), so it is never a slip.
52
+ function assessTurn({ wroteFiles = false, proposedAgent = false, fdActive = false } = {}) {
53
+ const slip = Boolean(wroteFiles && !proposedAgent && !fdActive);
54
+ return {
55
+ slip,
56
+ reason: slip
57
+ ? 'files written directly without an agent proposal and no active FD cycle'
58
+ : 'ok',
59
+ };
60
+ }
61
+
62
+ // --- slip flag (isolated I/O, mirrors the plain-language forward net) ------
63
+
64
+ function slipPath(projectDir) {
65
+ return path.join(projectDir, '_byan-output', '.agent-gate-slip.json');
66
+ }
67
+
68
+ function writeSlip(projectDir, detail) {
69
+ try {
70
+ const p = slipPath(projectDir);
71
+ fs.mkdirSync(path.dirname(p), { recursive: true });
72
+ fs.writeFileSync(p, JSON.stringify({ detail: String(detail || '') }));
73
+ return true;
74
+ } catch {
75
+ return false;
76
+ }
77
+ }
78
+
79
+ function readSlip(projectDir) {
80
+ try {
81
+ const parsed = JSON.parse(fs.readFileSync(slipPath(projectDir), 'utf8'));
82
+ return parsed && typeof parsed === 'object' ? parsed : null;
83
+ } catch {
84
+ return null;
85
+ }
86
+ }
87
+
88
+ function clearSlip(projectDir) {
89
+ try { fs.rmSync(slipPath(projectDir), { force: true }); } catch { /* never block */ }
90
+ }
91
+
92
+ // formatReminder — the plain-French note injected next turn. Empty when no slip.
93
+ function formatReminder(slip) {
94
+ if (!slip) return '';
95
+ return [
96
+ 'Rappel dispatch (porte d\'entree BYAN) : au dernier tour une tache a ete',
97
+ 'traitee en direct sans proposer d\'agent adapte. La base de BYAN, c\'est de',
98
+ 'passer par Hermes : proposer l\'agent qui colle (ou une interview pour en',
99
+ 'creer un), l\'utilisateur valide, PUIS on lance le travail. Applique-le ce tour-ci.',
100
+ ].join(' ');
101
+ }
102
+
103
+ // fdIsActive(projectDir) — reads _byan-output/fd-state.json ; active means a live
104
+ // phase (not COMPLETED / ABORTED / absent). Read-only, best-effort.
105
+ function fdIsActive(projectDir) {
106
+ try {
107
+ const state = JSON.parse(fs.readFileSync(path.join(projectDir, '_byan-output', 'fd-state.json'), 'utf8'));
108
+ const phase = state && state.phase;
109
+ return Boolean(phase) && phase !== 'COMPLETED' && phase !== 'ABORTED';
110
+ } catch {
111
+ return false;
112
+ }
113
+ }
114
+
115
+ module.exports = {
116
+ PROPOSAL_MARKERS,
117
+ WRITE_TOOLS,
118
+ hasProposalMarker,
119
+ hasWriteActivity,
120
+ assessTurn,
121
+ slipPath,
122
+ writeSlip,
123
+ readSlip,
124
+ clearSlip,
125
+ formatReminder,
126
+ fdIsActive,
127
+ };
@@ -0,0 +1,155 @@
1
+ 'use strict';
2
+
3
+ // Plain-language core — the shared, testable heart of the "parler reel" guard
4
+ // (Mantra IA-26). Two jobs, no I/O beyond the slip flag:
5
+ //
6
+ // 1. scanText: find the KNOWN repeat-offender words in an assistant reply and
7
+ // return each with its plain-French replacement. This is the mechanical net
8
+ // for words we already know we misuse; the generative rule (the principle in
9
+ // .claude/rules/plain-language.md + the voice anchor) handles the long tail.
10
+ // 2. slip flag: a tiny file under _byan-output/ that the Stop hook writes when a
11
+ // reply slipped, and the next-turn voice anchor reads + clears to remind the
12
+ // agent. NO re-answer, NO blocking — the correction is carried FORWARD to the
13
+ // next turn, which is the whole point (avoid the costly regen loop).
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+
18
+ // Known repeat-offenders -> plain French. Deliberately conservative: only terms
19
+ // with a clean everyday equivalent and a low false-positive risk once code is
20
+ // stripped. Common technical borrowings that have no French equivalent (commit,
21
+ // cache, token) are NOT here — the rule keeps them, explained once.
22
+ const OFFENDERS = Object.freeze([
23
+ { term: 'inline', good: 'directement (je le fais moi-meme)' },
24
+ { term: 'cutoff', good: "l'action reelle (ex: redemarrer, couper)" },
25
+ { term: 'housekeeping', good: 'rangement / menage du code' },
26
+ { term: 'downgrade', good: 'retrograder / baisser en gamme' },
27
+ { term: 'advisory', good: 'signalement non bloquant' },
28
+ { term: 'wrapper', good: 'enveloppe / surcouche' },
29
+ { term: 'fallback', good: 'repli / solution de secours' },
30
+ { term: 'throughput', good: 'debit' },
31
+ { term: 'overhead', good: 'surcout' },
32
+ { term: 'gate', good: 'point de controle / porte' },
33
+ { term: 'leaf', good: 'etape / tache' },
34
+ { term: 'tier', good: 'niveau / gamme' },
35
+ ]);
36
+
37
+ // A metaphor misuse, not a single word: "forger" applied to a token/jeton.
38
+ // Matched separately so the replacement can name the real verb.
39
+ const METAPHOR_OFFENDERS = Object.freeze([
40
+ {
41
+ id: 'forger-token',
42
+ re: /forg\w*\s+(?:un |une |des |le |les |la )?(?:token|jeton)/i,
43
+ label: 'forger un token',
44
+ good: 'generer / creer un token',
45
+ },
46
+ ]);
47
+
48
+ // A word boundary that treats accented letters as part of a word, so \btier\b
49
+ // style matching does NOT fire inside "metier", "chantier", "quartier"... The
50
+ // native \b is wrong here because JS \w excludes accented chars, splitting French
51
+ // words. This lookbehind/lookahead pair is the correct French-aware boundary.
52
+ const LEFT = '(?<![A-Za-zÀ-ÿ0-9_])';
53
+ const RIGHT = '(?![A-Za-zÀ-ÿ0-9_])';
54
+
55
+ function escapeRegExp(s) {
56
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
57
+ }
58
+
59
+ function termRegExp(term) {
60
+ return new RegExp(LEFT + escapeRegExp(term) + RIGHT, 'i');
61
+ }
62
+
63
+ // Remove fenced code blocks and inline `code` spans before scanning, so quoting a
64
+ // file, flag, or function literally named "tier" / "gate" does not trip the net.
65
+ // Prose is what we police; quoted code is not prose.
66
+ function stripCode(text) {
67
+ if (typeof text !== 'string' || !text) return '';
68
+ return text
69
+ .replace(/```[\s\S]*?```/g, ' ')
70
+ .replace(/`[^`]*`/g, ' ');
71
+ }
72
+
73
+ // Find every distinct offender present in the prose. Returns [{ bad, good }].
74
+ function scanText(text) {
75
+ const prose = stripCode(text);
76
+ if (!prose) return [];
77
+ const hits = [];
78
+ const seen = new Set();
79
+ for (const { term, good } of OFFENDERS) {
80
+ if (termRegExp(term).test(prose) && !seen.has(term)) {
81
+ seen.add(term);
82
+ hits.push({ bad: term, good });
83
+ }
84
+ }
85
+ for (const m of METAPHOR_OFFENDERS) {
86
+ if (m.re.test(prose) && !seen.has(m.id)) {
87
+ seen.add(m.id);
88
+ hits.push({ bad: m.label, good: m.good });
89
+ }
90
+ }
91
+ return hits;
92
+ }
93
+
94
+ // A short French reminder, for injection at the NEXT turn. Bounded to a few
95
+ // offenders so the note stays tiny even if a reply slipped many times.
96
+ function formatReminder(hits) {
97
+ if (!Array.isArray(hits) || hits.length === 0) return '';
98
+ const shown = hits.slice(0, 6)
99
+ .map((h) => `"${h.bad}" -> ${h.good}`)
100
+ .join(' ; ');
101
+ return [
102
+ 'Rappel langage (IA-26) : au dernier tour tu as glisse du jargon ou de',
103
+ `l'anglais gratuit -> ${shown}. Reformule en francais reel et coherent ce`,
104
+ 'tour-ci, sans refaire la reponse precedente.',
105
+ ].join(' ');
106
+ }
107
+
108
+ // The slip flag lives under _byan-output/ (gitignored), same family as the other
109
+ // hook sidecars. It is a transient one-turn signal, never source of truth.
110
+ function slipPath(projectDir) {
111
+ return path.join(projectDir, '_byan-output', '.jargon-slip.json');
112
+ }
113
+
114
+ function writeSlip(projectDir, hits) {
115
+ try {
116
+ const p = slipPath(projectDir);
117
+ fs.mkdirSync(path.dirname(p), { recursive: true });
118
+ fs.writeFileSync(p, JSON.stringify({ hits }));
119
+ return true;
120
+ } catch {
121
+ return false; // best-effort: a write failure must never block a turn
122
+ }
123
+ }
124
+
125
+ function readSlip(projectDir) {
126
+ try {
127
+ const raw = fs.readFileSync(slipPath(projectDir), 'utf8');
128
+ const parsed = JSON.parse(raw);
129
+ return Array.isArray(parsed.hits) ? parsed.hits : [];
130
+ } catch {
131
+ return null; // no flag (or unreadable) -> nothing to remind
132
+ }
133
+ }
134
+
135
+ function clearSlip(projectDir) {
136
+ try {
137
+ fs.rmSync(slipPath(projectDir), { force: true });
138
+ } catch {
139
+ // never block
140
+ }
141
+ }
142
+
143
+ module.exports = {
144
+ OFFENDERS,
145
+ METAPHOR_OFFENDERS,
146
+ escapeRegExp,
147
+ termRegExp,
148
+ stripCode,
149
+ scanText,
150
+ formatReminder,
151
+ slipPath,
152
+ writeSlip,
153
+ readSlip,
154
+ clearSlip,
155
+ };
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // Stop hook — the FORWARD net for the "parler reel" rule (Mantra IA-26).
5
+ //
6
+ // It reads the assistant reply that just finished, looks for the known
7
+ // jargon/anglais repeat-offenders, and — if it finds any — writes a one-turn
8
+ // slip flag under _byan-output/. It NEVER blocks the turn: there is no re-answer,
9
+ // no regen. The next-turn voice anchor (inject-voice-anchor.js) reads the flag,
10
+ // reminds the agent in plain French, and clears it. Carrying the correction
11
+ // FORWARD is deliberate: a blocking guard would force a costly regen AND the user
12
+ // has already read the slip anyway (no pre-display hook exists).
13
+ //
14
+ // Always exits 0. A read/scan/write failure degrades to a no-op.
15
+
16
+ const fs = require('fs');
17
+ const { extractLastAssistantText } = require('./lib/transcript-read');
18
+ const pl = require('./lib/plain-language');
19
+
20
+ // Testable core: scan the finished reply, flag a slip if any. Returns the hits.
21
+ function detectAndFlag(payload, projectDir) {
22
+ const text = extractLastAssistantText(payload);
23
+ const hits = pl.scanText(text);
24
+ if (hits.length > 0) pl.writeSlip(projectDir, hits);
25
+ return hits;
26
+ }
27
+
28
+ function readStdin() {
29
+ try {
30
+ return fs.readFileSync(0, 'utf8');
31
+ } catch {
32
+ return '';
33
+ }
34
+ }
35
+
36
+ if (require.main === module) {
37
+ const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
38
+ let payload = {};
39
+ try {
40
+ const raw = readStdin();
41
+ if (raw && raw.trim()) payload = JSON.parse(raw);
42
+ } catch {
43
+ payload = {};
44
+ }
45
+ try {
46
+ detectAndFlag(payload, projectDir);
47
+ } catch {
48
+ // never block end-of-turn
49
+ }
50
+ process.stdout.write('{}');
51
+ }
52
+
53
+ module.exports = { detectAndFlag };
@@ -0,0 +1,83 @@
1
+ # Porte d'entree BYAN — dispatch d'agent obligatoire (match-or-create)
2
+
3
+ > La base de BYAN : mettre la personne sur des rails pour toute tache ou projet.
4
+ > Avant de faire le travail, on regarde quel agent specialiste colle au besoin.
5
+ > Un agent adapte existe -> on le propose et on lance son workflow. Aucun ne
6
+ > colle -> on interviewe pour cadrer le besoin, on cherche les competences du
7
+ > metier, et on cree l'agent sur mesure. Le dispatch d'agent n'est pas une
8
+ > option : c'est l'entree de tout.
9
+
10
+ ## Le principe
11
+
12
+ Il y a DEUX dispatch, ne pas les confondre :
13
+ - **Dispatch d'agent (Hermes)** — QUI fait le travail : quel agent BYAN, ou on en
14
+ cree un. C'est le rail decrit ici.
15
+ - **Dispatch de runtime** — une fois l'agent choisi, sur quel moteur (Codex ou
16
+ Claude) + modele + effort. C'est la couche du dessous (voir
17
+ `docs/intelligent-dispatch.md`).
18
+
19
+ Cette regle porte sur le premier : la porte d'entree.
20
+
21
+ ## Le flux (sur toute tache non-conversationnelle)
22
+
23
+ ```
24
+ demande utilisateur
25
+ -> BYAN + Hermes evaluent quel agent specialiste colle (matcher F1)
26
+ AGENT ADAPTE TROUVE
27
+ -> BYAN le PROPOSE ("je pars sur @dev, tu valides ?")
28
+ -> l'utilisateur valide (double validation IA + humain)
29
+ -> on lance le workflow de cet agent (+ dispatch runtime)
30
+ AUCUN AGENT ADAPTE
31
+ -> BYAN le dit et propose une interview
32
+ -> interview : cadrer precisement le besoin de l'agent
33
+ -> recherche web : competences + bonnes pratiques du metier vise
34
+ -> creation de l'agent sur mesure
35
+ -> on lance le workflow
36
+ ```
37
+
38
+ Le declencheur de l'interview, c'est **l'absence d'un agent adapte**, pas la
39
+ taille de la tache. "code un script" -> l'agent dev existe -> workflow direct,
40
+ zero ceremonie. "il me faut un agent art moderne" -> aucun agent -> interview +
41
+ recherche + creation.
42
+
43
+ ## La proportion (pour que le rail ne devienne pas un mur)
44
+
45
+ Le rail impose de **verifier** la pertinence d'un agent a chaque tache — pas de
46
+ declencher une interview a chaque fois. Agent existant qui colle = on route
47
+ direct. Besoin flou ou aucun agent adapte = la ou l'interview + creation se
48
+ declenchent. La ceremonie est proportionnee au manque, pas systematique.
49
+
50
+ ## La double validation
51
+
52
+ Le matcher (F1, `_byan/mcp/byan-mcp-server/lib/agent-matcher.js`) est un
53
+ pre-tri deterministe : il classe les candidats du roster et rend un verdict
54
+ {fit | no-fit}. Il PROPOSE, il ne tranche pas seul. BYAN presente la proposition
55
+ en clair ; l'utilisateur valide. IA propose, humain confirme.
56
+
57
+ ## La creation d'agent enrichie (chemin no-fit)
58
+
59
+ Quand aucun agent ne colle, la creation passe par :
60
+ 1. **Interview** — cadrer le besoin (ce que l'agent doit faire, ses lignes
61
+ rouges, son perimetre). Flux INT / agent-builder existant.
62
+ 2. **Recherche web** — avant de generer l'agent, chercher les competences reelles
63
+ et les bonnes pratiques du metier vise (ex : pour un agent "art moderne",
64
+ courants, techniques, references). C'est ce qui evite l'agent-zombie generique.
65
+ 3. **Generation** — generer l'agent sur mesure a partir de l'interview + la
66
+ recherche.
67
+ 4. **Workflow** — lancer le travail avec le nouvel agent.
68
+
69
+ ## L'enforcement (honnete sur son plafond)
70
+
71
+ Aucun controle ne s'execute AVANT que la reponse s'affiche (pas de hook de
72
+ pre-affichage cote Claude Code). Le rail tient donc par deux forces, pas par un
73
+ mur beton au premier tour :
74
+ 1. **Doctrine** — cette regle + le skill byan-byan + `CLAUDE.md` font que BYAN
75
+ lance la proposition d'agent d'office.
76
+ 2. **Filet reactif** — `.claude/hooks/agent-gate-check.js` (Stop) repere une tache
77
+ traitee en direct (fichiers ecrits) sans proposition d'agent et hors cycle FD,
78
+ pose un drapeau, et le rappel du tour suivant le signale en clair. Non
79
+ bloquant : la correction est portee au tour d'apres (meme methode que les
80
+ autres filets BYAN). Coeur teste : `.claude/hooks/lib/agent-gate.js`.
81
+
82
+ Quand un cycle FD est deja engage, la porte est deja en jeu (la phase DISPATCH en
83
+ fait partie) : le filet ne se declenche pas.