create-byan-agent 2.48.0 → 2.50.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 (23) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/install/package.json +1 -1
  3. package/install/templates/.claude/CLAUDE.md +21 -13
  4. package/install/templates/.claude/hooks/agent-gate-check.js +14 -2
  5. package/install/templates/.claude/hooks/codex-autodelegate.js +19 -2
  6. package/install/templates/.claude/hooks/codex-delegate-guard.js +145 -0
  7. package/install/templates/.claude/hooks/inject-voice-anchor.js +25 -0
  8. package/install/templates/.claude/hooks/lib/agent-gate.js +95 -5
  9. package/install/templates/.claude/hooks/lib/autodelegate-decision.js +11 -7
  10. package/install/templates/.claude/hooks/lib/codex-delegate-gate.js +0 -0
  11. package/install/templates/.claude/hooks/lib/fact-check-core.js +34 -2
  12. package/install/templates/.claude/hooks/lib/voice-conformance.js +97 -0
  13. package/install/templates/.claude/hooks/strict-scope-guard.js +71 -14
  14. package/install/templates/.claude/hooks/voice-conformance-check.js +53 -0
  15. package/install/templates/.claude/rules/agent-entry-gate.md +40 -20
  16. package/install/templates/.claude/settings.json +8 -0
  17. package/install/templates/.claude/skills/byan-byan/SKILL.md +52 -26
  18. package/install/templates/_byan/mcp/byan-mcp-server/bin/byan-armament-report.js +82 -0
  19. package/install/templates/_byan/mcp/byan-mcp-server/lib/armament-report.js +80 -0
  20. package/install/templates/_byan/mcp/byan-mcp-server/skill-bundles-manifest.json +1 -1
  21. package/install/templates/dist/skill-bundles/byan-byan.zip +0 -0
  22. package/install/templates/docs/codex-auto-delegation.md +18 -3
  23. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -9,6 +9,56 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [2.50.0] - 2026-07-17
13
+
14
+ ### Added — BYAN souverainete (les concepts BYAN pilotent Claude, ne se font plus doubler)
15
+ - **WI-1 dent delegation Codex** : hook PreToolUse `codex-delegate-guard.js` (+ coeur pur `lib/codex-delegate-gate.js`) qui refuse-une-fois le premier Write/Edit de code delegable quand la voie Codex est armee (option + Codex linke) et qu'aucune delegation n'a eu lieu ce tour. Speed-bump, pas un mur : escape `.byan-codex-autodelegate/off`, marque `// BYAN-DELEGATE: reviewed`, fenetre de grace. Corrige la racine de l'incident (Claude codait lui-meme malgre la directive).
16
+ - **WI-2 filet de conformite de voix** : hook Stop `voice-conformance-check.js` (+ `lib/voice-conformance.js`) qui repere les signaux objectifs de derive (emoji, vouvoiement en grappe) et pose un drapeau relaye au tour suivant. Non bloquant (le registre reste semantique).
17
+ - **WI-3 filet dispatch runtime** : drapeau quand un tour ecrit du code sans avoir consulte `byan_dispatch`, hors FD.
18
+ - **WI-4 garde de porte d'entree renforce** : le signal visuel party-mode (`TaskCreate`/`TaskUpdate`) tient desormais la posture d'entree — plus de faux drapeau sur un tour party-mode.
19
+ - **WI-7 rapport d'armement** : `bin/byan-armament-report.js` (+ `lib/armament-report.js`) lit les registres autobench/punt/completeness et reporte le taux de declenchement (indicateur de risque de faux positif) avant toute decision d'armement. Verdict mesure : les 3 gardes restent DESARMEES (punt 42%, autobench 1%, completeness echantillon trop petit).
20
+
21
+ ### Changed
22
+ - **WI-6 fuite Bash du strict-scope-guard** : `decideScope` couvre desormais les redirections d'ecriture Bash (`>`, `>>`, `tee`, heredoc) hors `allowedPaths`. Parseur resserre (cible en forme de chemin uniquement) pour eviter les faux positifs sur les operateurs de comparaison/arithmetique et les variables non resolues.
23
+ - **WI-5 fact-check-absolutes elargi** : plus d'absolus/superlatifs/best-practice/certitudes couverts (alignes sur `.claude/rules/fact-check.md`) ; `optimal` ecarte (trop courant) et strip d'exemples resserre a une puce (une phrase de prose commencant par un absolu reste policee). Le plafond des mantras semantiques (IA-16, #37...) est assume : doctrine + `byan-mantra-audit`, pas de fausse dent regex.
24
+
25
+ ### Docs
26
+ - `docs/byan-sovereignty-chantier.md` : le chantier complet (constat, patron des 4 dents, plafond honnete, work items, risques) adosse a l'audit `byan-sovereignty-audit`.
27
+
28
+ ### Tests
29
+ - +9 tests MCP (armament report + parite shipping anti-derive) ; +47 tests jest (dents + filets + fact-check + strict-scope Bash). jest 2699/0, MCP 855/0.
30
+
31
+ ## [2.49.0] - 2026-07-16
32
+
33
+ ### Added
34
+ - Chaine d'entree BYAN v2 dans le skill `byan-byan` (section 0) : comprendre la
35
+ demande -> point de controle de plan uniquement sur doute -> dispatch Hermes
36
+ automatique (agent + modele + effort) -> execution party-mode avec visuel live
37
+ -> point de controle utilisateur en fin. L'humain reste requis seulement pour
38
+ creer un nouvel agent, confirmer un destructif, ou trancher un plan en cas de
39
+ doute.
40
+ - Visuel party-mode cable sur la liste de taches native (une entree par
41
+ agent/etape, `TaskCreate` / `TaskUpdate` en direct) plus la table de dispatch
42
+ affichee en tete de la phase BUILD.
43
+ - Garde "les deux moteurs" (`codexLinked()`) dans `codex-autodelegate.js` : la
44
+ voie Codex ne s'arme que si l'option yanstaller est activee ET Codex est linke
45
+ (`~/.codex/auth.json` ou `CODEX_API_KEY`) ; option seule = pas de delegation.
46
+
47
+ ### Changed
48
+ - La delegation Codex sur voie armee passe de "conseil advisory" a "directive" :
49
+ quand la voie est armee (option + les deux moteurs), le rendu injecte demande
50
+ de deleguer le delegable via `codex-bridge` (repli Claude si Codex
51
+ indisponible), au lieu de laisser BYAN executer sur Claude par defaut.
52
+ - Doctrine synchronisee (`.claude/rules/agent-entry-gate.md`, `.claude/CLAUDE.md`)
53
+ a la chaine d'entree v2 ; la double validation est calibree (doute / nouvel
54
+ agent / destructif) au lieu d'un reflexe a chaque tache.
55
+
56
+ ### Docs
57
+ - `docs/codex-auto-delegation.md` : garde "les deux moteurs", directive-quand-arme,
58
+ plafond honnete.
59
+ - `docs/per-platform-projection-opti.md` : chantier suivant planifie (coeur unique
60
+ + deux projections optimisees par plateforme Claude/Codex, sans source rivale).
61
+
12
62
  ## [2.48.0] - 2026-07-16
13
63
 
14
64
  ### Added - The whole dispatch chain now runs automatically at the entry
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-byan-agent",
3
- "version": "2.37.1",
3
+ "version": "2.50.0",
4
4
  "description": "BYAN - Intelligent AI agent installer with multi-platform native support (Claude Code, Codex/OpenCode)",
5
5
  "bin": {
6
6
  "create-byan-agent": "bin/create-byan-agent-v2.js"
@@ -17,19 +17,27 @@ Voir @.claude/rules/hermes-dispatcher.md pour les commandes Hermes.
17
17
 
18
18
  ## Porte d'entree — dispatch d'agent obligatoire (match-or-create)
19
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.
27
-
28
- A l'entree, BYAN enchaine AUTOMATIQUEMENT toute la chaine, sans que tu la
29
- demandes : (1) quel agent, (2) quel moteur — Codex pour execution/shell/deploy/
30
- devops/navigateur, Claude sinon (via `dispatch-router`), (3) execution, avec
31
- delegation reelle a Codex sur sa voie. L'humain reste requis seulement pour creer
32
- un nouvel agent et confirmer une action destructive. Detail + enforcement : voir
20
+ La base de BYAN : toute tache non-conversationnelle passe d'abord par la chaine
21
+ d'entree. A l'entree, BYAN enchaine AUTOMATIQUEMENT toute la chaine, sans que tu
22
+ la demandes quatre moments :
23
+
24
+ 1. **Comprendre.** Demande claire -> on avance directement, zero question. Doute
25
+ ou demande mal exprimee -> BYAN expose UN plan clair (quels agents, quels
26
+ modeles, quel effort) et tu tranches. Ce point de controle en amont ne se
27
+ declenche que sur un vrai doute.
28
+ 2. **Dispatch (Hermes, automatique).** Le bon agent, le bon modele, le bon effort,
29
+ le bon moteur — Codex pour execution/shell/deploy/devops/navigateur, Claude
30
+ sinon (via `dispatch-router`). Aucun agent adapte -> interview + recherche web
31
+ (competences + bonnes pratiques du metier) -> creation de l'agent sur mesure.
32
+ 3. **Execution en party-mode avec visuel live.** Tu vois ce qui se passe en
33
+ direct (liste de taches native + table de dispatch). Voie Codex armee (option
34
+ yanstaller + Codex linke) + tache delegable -> delegation reelle a Codex ;
35
+ sinon execution sur Claude.
36
+ 4. **Gate en fin.** La validation utilisateur arrive a la fin, sur le livrable —
37
+ pas en amont.
38
+
39
+ L'humain reste requis seulement pour : creer un nouvel agent, confirmer une action
40
+ destructive, trancher le plan quand il y a doute. Detail + enforcement : voir
33
41
  @.claude/rules/agent-entry-gate.md et @docs/intelligent-dispatch.md
34
42
 
35
43
  ## Architecture BYAN
@@ -20,12 +20,24 @@ const gate = require('./lib/agent-gate');
20
20
  function detect(payload, projectDir) {
21
21
  const text = extractLastAssistantText(payload);
22
22
  const messages = extractRecentMessages(payload, 8) || [];
23
+ const wroteFiles = gate.hasWriteActivity(messages);
24
+ const fdActive = gate.fdIsActive(projectDir);
23
25
  const assessment = gate.assessTurn({
24
- wroteFiles: gate.hasWriteActivity(messages),
26
+ wroteFiles,
25
27
  proposedAgent: gate.hasProposalMarker(text),
26
- fdActive: gate.fdIsActive(projectDir),
28
+ taskVisualSeen: gate.hasTaskActivity(messages), // WI-4: a live task list holds the entry posture
29
+ fdActive,
27
30
  });
28
31
  if (assessment.slip) gate.writeSlip(projectDir, assessment.reason);
32
+ // WI-3: dispatch net, its own slip. Same signals, different question (was the
33
+ // runtime routeur consulted?).
34
+ const dispatch = gate.assessDispatch({
35
+ wroteFiles,
36
+ dispatchConsulted: gate.hasDispatchActivity(messages),
37
+ fdActive,
38
+ });
39
+ if (dispatch.slip) gate.writeDispatchSlip(projectDir, dispatch.reason);
40
+ assessment.dispatch = dispatch; // backward-compatible: .slip stays primary
29
41
  return assessment;
30
42
  }
31
43
 
@@ -20,6 +20,7 @@
20
20
 
21
21
  const fs = require('fs');
22
22
  const path = require('path');
23
+ const os = require('os');
23
24
  const { readStdin, parseJson } = require('./lib/strict-runtime');
24
25
  const { decideAutodelegation, renderNudge } = require('./lib/autodelegate-decision');
25
26
  const { estimateClaudeUsage } = require('./lib/usage-estimator');
@@ -44,13 +45,29 @@ function toggledOff(projectRoot) {
44
45
  }
45
46
  }
46
47
 
48
+ // The "both engines present" gate. The lane arms only when the yanstaller option
49
+ // wrote enabled:true (checked via loadConfig) AND Codex is actually linked on this
50
+ // machine — a ChatGPT subscription via `codex login` (~/.codex/auth.json) or
51
+ // CODEX_API_KEY. Option on but no linked Codex = one engine only = no delegation
52
+ // (the user's rule: option AND both engines, else nothing). Mirrors codexAuthState
53
+ // in install/lib/codex-autodelegate-setup.js.
54
+ function codexLinked({ home = os.homedir() } = {}) {
55
+ if (process.env.CODEX_API_KEY) return true;
56
+ try {
57
+ return fs.existsSync(path.join(home, '.codex', 'auth.json'));
58
+ } catch {
59
+ return false;
60
+ }
61
+ }
62
+
47
63
  if (require.main === module) {
48
64
  (async () => {
49
65
  let additionalContext = '';
50
66
  try {
51
67
  const projectRoot = process.env.CLAUDE_PROJECT_DIR || process.cwd();
52
68
  const config = loadConfig(projectRoot);
53
- if (config.enabled === true && !toggledOff(projectRoot)) {
69
+ // Both engines must be present : option armed (enabled) AND Codex linked.
70
+ if (config.enabled === true && !toggledOff(projectRoot) && codexLinked()) {
54
71
  const payload = parseJson(await readStdin());
55
72
  const prompt = payload.prompt || payload.user_prompt || payload.userPrompt || '';
56
73
  // Estimate usage only when a budget is configured; else pct stays null and
@@ -71,4 +88,4 @@ if (require.main === module) {
71
88
  })();
72
89
  }
73
90
 
74
- module.exports = { loadConfig, toggledOff };
91
+ module.exports = { loadConfig, toggledOff, codexLinked };
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // PreToolUse hook — WI-1, the DENT for the armed Codex-delegation lane.
5
+ //
6
+ // When the Codex lane is ARMED (yanstaller option on AND Codex linked) and the
7
+ // current turn is about to WRITE delegable code without having delegated to Codex
8
+ // first, DENY ONCE with the exact instruction to delegate. This is the tooth that
9
+ // makes "delegate to Codex when armed" govern instead of being prose Claude can
10
+ // double with a polite excuse (the session incident).
11
+ //
12
+ // Speed-bump, not a wall (the user's red line) :
13
+ // - escape-hatch : `.byan-codex-autodelegate/off` silences it (shared with the
14
+ // nudge hook — one switch for the whole Codex-delegation posture).
15
+ // - `// BYAN-DELEGATE: reviewed` in the file acknowledges a deliberate
16
+ // Claude-lane choice (Codex unavailable, judgment/verify) and passes.
17
+ // - deny-once : an identical resubmission passes, and a grace window after a
18
+ // deny lets a multi-file build proceed without nagging on every file.
19
+ //
20
+ // Never traps a turn, always exits 0. Pure decision in lib/codex-delegate-gate.js;
21
+ // armed/linked detection reused from codex-autodelegate.js; delegability from
22
+ // autodelegate-decision.js; the turn transcript via transcript-read.js.
23
+
24
+ const fs = require('fs');
25
+ const path = require('path');
26
+ const gate = require('./lib/codex-delegate-gate');
27
+ const { loadConfig, codexLinked, toggledOff } = require('./codex-autodelegate');
28
+ const { looksDelegable } = require('./lib/autodelegate-decision');
29
+ const { extractRecentMessages, extractLastAssistantText, contentToText } = require('./lib/transcript-read');
30
+
31
+ const ROOT = process.env.CLAUDE_PROJECT_DIR || process.cwd();
32
+ const GRACE_MS = 120000;
33
+
34
+ function readStdin() {
35
+ return new Promise((resolve) => {
36
+ let data = '';
37
+ process.stdin.on('data', (c) => (data += c));
38
+ process.stdin.on('end', () => resolve(data));
39
+ process.stdin.on('error', () => resolve(''));
40
+ });
41
+ }
42
+
43
+ function allow() {
44
+ return { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'allow' } };
45
+ }
46
+
47
+ function deny(reason) {
48
+ return {
49
+ hookSpecificOutput: {
50
+ hookEventName: 'PreToolUse',
51
+ permissionDecision: 'deny',
52
+ permissionDecisionReason: reason,
53
+ },
54
+ };
55
+ }
56
+
57
+ function sidecarPath(root) {
58
+ return path.join(root, '.byan-codex-delegate', 'last-deny.json');
59
+ }
60
+
61
+ function readPriorDeny(root) {
62
+ try {
63
+ const p = JSON.parse(fs.readFileSync(sidecarPath(root), 'utf8'));
64
+ return p && typeof p === 'object' ? p : null;
65
+ } catch (_e) {
66
+ return null;
67
+ }
68
+ }
69
+
70
+ function writePriorDeny(root, entry) {
71
+ try {
72
+ fs.mkdirSync(path.dirname(sidecarPath(root)), { recursive: true });
73
+ fs.writeFileSync(sidecarPath(root), JSON.stringify(entry));
74
+ } catch (_e) {
75
+ // best-effort : losing the deny memory only costs one extra deny, never a trap
76
+ }
77
+ }
78
+
79
+ // The last user message text this turn, for the delegability signal (a code
80
+ // target already suffices, this only broadens it).
81
+ function lastUserText(payload) {
82
+ const msgs = extractRecentMessages(payload, 12);
83
+ if (!Array.isArray(msgs)) return '';
84
+ for (let i = msgs.length - 1; i >= 0; i--) {
85
+ if (msgs[i] && msgs[i].role === 'user') return contentToText(msgs[i].content);
86
+ }
87
+ return '';
88
+ }
89
+
90
+ // Pure-ish assembly of the decision from a payload (exported for tests, fs
91
+ // injected via opts.root + opts.now).
92
+ function runGuard(payload, { root = ROOT, now = Date.now() } = {}) {
93
+ const toolName = payload.tool_name || payload.toolName || '';
94
+ if (toolName !== 'Write' && toolName !== 'Edit') return allow();
95
+
96
+ const config = loadConfig(root);
97
+ const armed = config.enabled === true && codexLinked();
98
+ const escaped = toggledOff(root);
99
+
100
+ const input = payload.tool_input || payload.toolInput || {};
101
+ const filePath = input.file_path || input.filePath || '';
102
+ const content = toolName === 'Edit'
103
+ ? String(input.new_string || input.newString || '')
104
+ : String(input.content || '');
105
+
106
+ const userText = lastUserText(payload);
107
+ const delegable = gate.targetIsCode(filePath) || looksDelegable(userText);
108
+ const reviewedMarker = gate.hasReviewedMarker(content) || gate.hasReviewedMarker(extractLastAssistantText(payload));
109
+ const delegationSeen = gate.delegationSeenThisTurn(extractRecentMessages(payload, 12) || []);
110
+ const turnHash = gate.hashTurn(filePath, content);
111
+
112
+ const decision = gate.decideDelegateGate({
113
+ armed,
114
+ delegable,
115
+ delegationSeen,
116
+ escaped,
117
+ reviewedMarker,
118
+ priorDeny: readPriorDeny(root),
119
+ turnHash,
120
+ now,
121
+ graceMs: GRACE_MS,
122
+ });
123
+
124
+ if (decision.decision === 'deny') {
125
+ writePriorDeny(root, { hash: turnHash, ts: now });
126
+ return deny(decision.reason);
127
+ }
128
+ return allow();
129
+ }
130
+
131
+ if (require.main === module) {
132
+ (async () => {
133
+ let out;
134
+ try {
135
+ const payload = JSON.parse((await readStdin()) || '{}');
136
+ out = runGuard(payload);
137
+ } catch (_e) {
138
+ out = allow();
139
+ }
140
+ process.stdout.write(JSON.stringify(out));
141
+ process.exit(0);
142
+ })();
143
+ }
144
+
145
+ module.exports = { runGuard, sidecarPath, readPriorDeny, writePriorDeny, lastUserText };
@@ -28,6 +28,7 @@ const path = require('path');
28
28
  const { buildTaoContext, turnCounterPath } = require('./inject-tao');
29
29
  const pl = require('./lib/plain-language');
30
30
  const gate = require('./lib/agent-gate');
31
+ const voice = require('./lib/voice-conformance');
31
32
 
32
33
  const ANCHOR = [
33
34
  'Voix BYAN (rappel par tour ; tao complet chargé au démarrage de session) :',
@@ -96,6 +97,20 @@ function withGateReminder(baseContext, slip) {
96
97
  return reminder ? `${baseContext}\n${reminder}` : baseContext;
97
98
  }
98
99
 
100
+ // Append the dispatch reminder (WI-3) when the previous turn wrote code without
101
+ // consulting byan_dispatch. Pure; fs read + clear stays in require.main.
102
+ function withDispatchReminder(baseContext, slip) {
103
+ const reminder = gate.formatDispatchReminder(slip);
104
+ return reminder ? `${baseContext}\n${reminder}` : baseContext;
105
+ }
106
+
107
+ // Append the voice-conformance reminder (WI-2) when the previous turn drifted from
108
+ // the BYAN voice (emoji / vouvoiement). Pure; fs read + clear stays in require.main.
109
+ function withVoiceReminder(baseContext, hits) {
110
+ const reminder = voice.formatReminder(hits);
111
+ return reminder ? `${baseContext}\n${reminder}` : baseContext;
112
+ }
113
+
99
114
  if (require.main === module) {
100
115
  const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
101
116
  const every = refreshEvery();
@@ -110,8 +125,16 @@ if (require.main === module) {
110
125
  // Forward net #2 (F4): agent entry-gate slip. Read + clear one-shot.
111
126
  const gateSlip = gate.readSlip(projectDir);
112
127
  if (gateSlip) gate.clearSlip(projectDir);
128
+ // Forward net #3 (WI-3): dispatch-runtime slip. Read + clear one-shot.
129
+ const dispatchSlip = gate.readDispatchSlip(projectDir);
130
+ if (dispatchSlip) gate.clearDispatchSlip(projectDir);
131
+ // Forward net #4 (WI-2): voice-conformance slip. Read + clear one-shot.
132
+ const voiceHits = voice.readSlip(projectDir);
133
+ if (voiceHits && voiceHits.length) voice.clearSlip(projectDir);
113
134
  let ctx = withSlipReminder(additionalContext, slipHits);
114
135
  ctx = withGateReminder(ctx, gateSlip);
136
+ ctx = withDispatchReminder(ctx, dispatchSlip);
137
+ ctx = withVoiceReminder(ctx, voiceHits);
115
138
  process.stdout.write(
116
139
  JSON.stringify({
117
140
  hookSpecificOutput: {
@@ -131,5 +154,7 @@ module.exports = {
131
154
  decideAnchor,
132
155
  withSlipReminder,
133
156
  withGateReminder,
157
+ withDispatchReminder,
158
+ withVoiceReminder,
134
159
  DEFAULT_REFRESH_EVERY,
135
160
  };
@@ -32,6 +32,33 @@ function hasProposalMarker(text) {
32
32
  return PROPOSAL_MARKERS.some((m) => t.includes(m));
33
33
  }
34
34
 
35
+ // hasToolActivity(messages, predicate) — true if any assistant tool_use block in
36
+ // the recent messages matches the predicate(block). The shared scan used by the
37
+ // write / task-visual / dispatch detectors.
38
+ function hasToolActivity(messages, predicate) {
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' && predicate(block)) return true;
44
+ }
45
+ }
46
+ return false;
47
+ }
48
+
49
+ // WI-4 — the party-mode live visual signal : did the turn create a native task
50
+ // (TaskCreate) ? A task list IS the visual the entry chain promises, so its
51
+ // presence means the porte d'entree posture held even without a text proposal.
52
+ function hasTaskActivity(messages) {
53
+ return hasToolActivity(messages, (b) => b.name === 'TaskCreate' || b.name === 'TaskUpdate');
54
+ }
55
+
56
+ // WI-3 — the dispatch-consulted signal : was the routeur (byan_dispatch) called
57
+ // this turn ? Matches the MCP tool name in any namespaced form.
58
+ function hasDispatchActivity(messages) {
59
+ return hasToolActivity(messages, (b) => /byan_dispatch/.test(String(b.name || '')));
60
+ }
61
+
35
62
  // hasWriteActivity(messages) — true if any recent assistant message carried a
36
63
  // tool_use block that writes to the repo. `messages` is an array of
37
64
  // { role, content } where content is a string or a block array.
@@ -47,14 +74,28 @@ function hasWriteActivity(messages) {
47
74
  }
48
75
 
49
76
  // 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);
77
+ // no agent proposal, NO party-mode task visual (WI-4), AND no active FD cycle.
78
+ // When an FD is engaged, the gate is already in play (DISPATCH is part of it), so
79
+ // it is never a slip ; likewise a live task list is proof the entry posture held.
80
+ function assessTurn({ wroteFiles = false, proposedAgent = false, taskVisualSeen = false, fdActive = false } = {}) {
81
+ const slip = Boolean(wroteFiles && !proposedAgent && !taskVisualSeen && !fdActive);
54
82
  return {
55
83
  slip,
56
84
  reason: slip
57
- ? 'files written directly without an agent proposal and no active FD cycle'
85
+ ? 'files written directly without an agent proposal, no task visual, and no active FD cycle'
86
+ : 'ok',
87
+ };
88
+ }
89
+
90
+ // WI-3 — dispatch net. A slip is code written this turn without the runtime
91
+ // routeur (byan_dispatch) ever consulted, outside an FD cycle. Non-blocking : the
92
+ // engine choice is a decision, not a binary invariant, so this only flags.
93
+ function assessDispatch({ wroteFiles = false, dispatchConsulted = false, fdActive = false } = {}) {
94
+ const slip = Boolean(wroteFiles && !dispatchConsulted && !fdActive);
95
+ return {
96
+ slip,
97
+ reason: slip
98
+ ? 'code written without consulting byan_dispatch (runtime routing) and no active FD cycle'
58
99
  : 'ok',
59
100
  };
60
101
  }
@@ -100,6 +141,46 @@ function formatReminder(slip) {
100
141
  ].join(' ');
101
142
  }
102
143
 
144
+ // --- dispatch slip (WI-3, its own sidecar, same forward-net mechanics) --------
145
+
146
+ function dispatchSlipPath(projectDir) {
147
+ return path.join(projectDir, '_byan-output', '.dispatch-slip.json');
148
+ }
149
+
150
+ function writeDispatchSlip(projectDir, detail) {
151
+ try {
152
+ const p = dispatchSlipPath(projectDir);
153
+ fs.mkdirSync(path.dirname(p), { recursive: true });
154
+ fs.writeFileSync(p, JSON.stringify({ detail: String(detail || '') }));
155
+ return true;
156
+ } catch {
157
+ return false;
158
+ }
159
+ }
160
+
161
+ function readDispatchSlip(projectDir) {
162
+ try {
163
+ const parsed = JSON.parse(fs.readFileSync(dispatchSlipPath(projectDir), 'utf8'));
164
+ return parsed && typeof parsed === 'object' ? parsed : null;
165
+ } catch {
166
+ return null;
167
+ }
168
+ }
169
+
170
+ function clearDispatchSlip(projectDir) {
171
+ try { fs.rmSync(dispatchSlipPath(projectDir), { force: true }); } catch { /* never block */ }
172
+ }
173
+
174
+ function formatDispatchReminder(slip) {
175
+ if (!slip) return '';
176
+ return [
177
+ 'Rappel dispatch runtime (BYAN) : au dernier tour du code a ete ecrit sans',
178
+ 'consulter byan_dispatch (le routeur moteur/modele/effort). La chaine d\'entree',
179
+ 'route avant d\'executer : passe par byan_dispatch pour choisir Codex ou Claude,',
180
+ 'le modele et l\'effort. Applique-le ce tour-ci.',
181
+ ].join(' ');
182
+ }
183
+
103
184
  // fdIsActive(projectDir) — reads _byan-output/fd-state.json ; active means a live
104
185
  // phase (not COMPLETED / ABORTED / absent). Read-only, best-effort.
105
186
  function fdIsActive(projectDir) {
@@ -117,11 +198,20 @@ module.exports = {
117
198
  WRITE_TOOLS,
118
199
  hasProposalMarker,
119
200
  hasWriteActivity,
201
+ hasToolActivity,
202
+ hasTaskActivity,
203
+ hasDispatchActivity,
120
204
  assessTurn,
205
+ assessDispatch,
121
206
  slipPath,
122
207
  writeSlip,
123
208
  readSlip,
124
209
  clearSlip,
125
210
  formatReminder,
211
+ dispatchSlipPath,
212
+ writeDispatchSlip,
213
+ readDispatchSlip,
214
+ clearDispatchSlip,
215
+ formatDispatchReminder,
126
216
  fdIsActive,
127
217
  };
@@ -123,22 +123,26 @@ function sanitizeForContext(value, max = 120) {
123
123
  }
124
124
 
125
125
  // Render a decision into the one-paragraph nudge injected into BYAN's context.
126
- // Empty string when there is nothing to propose (so the hook injects nothing).
127
- // The nudge is ADVISORY: it proposes, it never forces BYAN still owns the call
128
- // and the red line is spelled out every time.
126
+ // Empty string when there is nothing to render (so the hook injects nothing).
127
+ // The hook injects this ONLY when the lane is armed (option on AND Codex linked),
128
+ // so the text is a DIRECTIVE, not advice: delegate the delegable part, fall back
129
+ // to Claude only if Codex is unavailable. The red line (delegable work only —
130
+ // judgment/soul/verify stay on Claude) is spelled out every time.
129
131
  function renderNudge(decision) {
130
132
  if (!decision || !decision.delegate) return '';
131
133
  const gauge = decision.pct != null ? ` (estimated Claude 5h usage ~${decision.pct}%)` : '';
132
134
  const invocation = sanitizeForContext(decision.invocation) || DEFAULT_INVOCATION;
133
135
  const scope = decision.mode === 'all'
134
- ? 'Consider offloading ALL delegable work this session to Codex'
136
+ ? 'Delegate ALL delegable work this session to Codex'
135
137
  : decision.mode === 'perf-routed'
136
- ? 'Codex is heuristically favored for this kind of task (not a measured benchmark) — consider handing it over'
137
- : 'This looks like delegable coding work — consider handing it to Codex';
138
+ ? 'Codex is heuristically favored for this kind of task (not a measured benchmark) — delegate it'
139
+ : 'This is delegable coding work — delegate it to Codex';
138
140
  return [
139
141
  `[BYAN auto-delegate]${gauge}: ${scope} via \`${invocation}\` `
140
142
  + '(runs on the ChatGPT subscription, no API credit).',
141
- `Red line: ${decision.redLine}. This is advisory you decide, and you still verify Codex's output before commit.`,
143
+ `Red line: ${decision.redLine}. The lane is ARMED (option on AND Codex linked), so this is a `
144
+ + 'DIRECTIVE, not a mere suggestion: delegate the delegable part unless you have a specific reason '
145
+ + "not to, fall back to Claude only if Codex is unavailable, and verify Codex's output before commit.",
142
146
  ].join(' ');
143
147
  }
144
148
 
@@ -8,7 +8,14 @@
8
8
 
9
9
  'use strict';
10
10
 
11
+ // WI-5 — the absolute-claim patterns. Kept HIGH-SIGNAL on purpose : this gate
12
+ // hard-blocks a doc write, so a false positive costs the user a block. Every
13
+ // entry is either a plain absolute, a comparative superlative, an unsourced
14
+ // best-practice claim, or a certainty phrase — the exact trigger families named
15
+ // in .claude/rules/fact-check.md. Noisy bare words ("mieux", "the best",
16
+ // "impossible", "bien sur") are deliberately excluded.
11
17
  const ABSOLUTES = [
18
+ // plain absolutes
12
19
  /\btoujours\b/i,
13
20
  /\bjamais\b/i,
14
21
  /\bforc[eé]ment\b/i,
@@ -17,10 +24,31 @@ const ABSOLUTES = [
17
24
  /\bnever\b/i,
18
25
  /\bclearly\b/i,
19
26
  /\bundoubtedly\b/i,
27
+ /\b[ée]videmment\b/i,
28
+ /\bcertainly\b/i,
29
+ /\bwithout a doubt\b/i,
30
+ /\bsans aucun doute\b/i,
31
+ /\bguaranteed\b/i,
32
+ // comparative superlatives (a claim, not phrasing)
20
33
  /\bfaster than\b/i,
21
34
  /\bbetter than\b/i,
22
35
  /\bplus rapide que\b/i,
23
36
  /\bmeilleur que\b/i,
37
+ /\bthe fastest\b/i,
38
+ /\ble plus rapide\b/i,
39
+ /\bsuperior to\b/i,
40
+ // ("optimal" alone was dropped in WI-5 review : too common in legit doc prose
41
+ // ("the optimal layout") to hard-block a doc write without a false positive.)
42
+ // unsourced best-practice / authority claims
43
+ /\bbest practice\b/i,
44
+ /\bbonne pratique\b/i,
45
+ /\bindustry standard\b/i,
46
+ /\bstandard de l['’]industrie\b/i,
47
+ // certainty framing
48
+ /\bil est clair que\b/i,
49
+ /\bit is well[- ]known\b/i,
50
+ /\bwell[- ]known that\b/i,
51
+ /\bprouv[eé] que\b/i,
24
52
  ];
25
53
 
26
54
  const SOURCE_MARKERS = [
@@ -37,14 +65,18 @@ const SOURCE_MARKERS = [
37
65
  // - fenced code blocks ``` ... ```
38
66
  // - inline backticks `...`
39
67
  // - block quotes (lines starting with >)
40
- // - list-of-pattern lines (e.g. "- toujours")
68
+ // - BULLET list-example lines (e.g. "- toujours") — a doc listing the trigger
69
+ // words as examples, not asserting them. WI-5 review fix : this REQUIRES a
70
+ // bullet marker (- or *), so a prose sentence merely STARTING with an
71
+ // absolute ("Never mutate state directly.") is NOT stripped and stays policed
72
+ // (the old `^[\s-]*` matched any leading whitespace -> a false negative).
41
73
  function stripNonClaimZones(text) {
42
74
  if (!text) return '';
43
75
  return text
44
76
  .replace(/```[\s\S]*?```/g, '')
45
77
  .replace(/`[^`\n]+`/g, '')
46
78
  .replace(/^> .*$/gm, '')
47
- .replace(/^[\s-]*['"]?\b(toujours|jamais|forc[eé]ment|obviously|always|never|clearly|undoubtedly)\b['"]?/gim, '');
79
+ .replace(/^\s*[-*]\s+['"]?\b(toujours|jamais|forc[eé]ment|obviously|always|never|clearly|undoubtedly|[ée]videmment|certainly|guaranteed)\b['"]?/gim, '');
48
80
  }
49
81
 
50
82
  // Return the first unsourced absolute (with surrounding context) or null when