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
@@ -0,0 +1,97 @@
1
+ 'use strict';
2
+
3
+ // WI-2 core — the reactive net for BYAN voice conformance (soul/tao).
4
+ //
5
+ // The tao is injected as context (inject-tao / voice-anchor) but nothing checks
6
+ // that a reply actually holds the voice : it is prose Claude can drift from. This
7
+ // net scans the finished reply for the OBJECTIVE, low-false-positive voice
8
+ // signals and flags a slip carried to the next turn — the same forward-net
9
+ // mechanics as plain-language / agent-gate. It is NON-BLOCKING by design : the
10
+ // register/timbre of the tao is semantic and cannot be a hard wall without
11
+ // constant false positives (honest ceiling ; deep audit stays byan-mantra-audit).
12
+ //
13
+ // Two objective signals only :
14
+ // 1. emoji in the reply (Mantra IA-23 : zero emoji — hard, unambiguous).
15
+ // 2. vouvoiement (BYAN tutoies always, per tao) — flagged only on a CLUSTER
16
+ // (>= 2 occurrences) so a single quoted "vous" does not trip it.
17
+ //
18
+ // Pure (no I/O beyond the slip flag). Code spans are stripped before scanning so
19
+ // a quoted `vous` variable or an emoji inside a code sample is not policed.
20
+
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+ const { stripCode } = require('./plain-language');
24
+
25
+ // True pictographic emoji (Mantra IA-23). Excludes plain arrows (U+2190-21FF)
26
+ // which BYAN uses legitimately in prose ("->", "→").
27
+ const EMOJI_RE = /[\u{1F000}-\u{1FAFF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{2B00}-\u{2BFF}\u{1F1E6}-\u{1F1FF}\u{FE0F}]/u;
28
+
29
+ // Second-person-plural address. French-aware boundaries so "nous"/"vousXYZ" and
30
+ // accented neighbours do not false-match.
31
+ const VOUS_RE = /(?<![A-Za-zÀ-ÿ0-9_])(vous|votre|vos|vôtre|vôtres)(?![A-Za-zÀ-ÿ0-9_])/gi;
32
+
33
+ // scanVoice(text) -> [{ kind, good }]. Empty when the reply holds the voice.
34
+ function scanVoice(text) {
35
+ const prose = stripCode(text);
36
+ if (!prose) return [];
37
+ const hits = [];
38
+ if (EMOJI_RE.test(prose)) {
39
+ hits.push({ kind: 'emoji', good: 'zero emoji (Mantra IA-23) — retire-les' });
40
+ }
41
+ const vousCount = (prose.match(VOUS_RE) || []).length;
42
+ if (vousCount >= 2) {
43
+ hits.push({ kind: 'vouvoiement', good: 'BYAN tutoie toujours (tao) — dis "tu", pas "vous"' });
44
+ }
45
+ return hits;
46
+ }
47
+
48
+ function formatReminder(hits) {
49
+ if (!Array.isArray(hits) || hits.length === 0) return '';
50
+ const shown = hits.map((h) => `${h.kind} (${h.good})`).join(' ; ');
51
+ return [
52
+ 'Rappel voix (tao / soul) : au dernier tour la voix BYAN a glisse ->', `${shown}.`,
53
+ 'Reformule ce tour-ci dans la voix BYAN (tutoiement, zero emoji), sans refaire',
54
+ 'la reponse precedente.',
55
+ ].join(' ');
56
+ }
57
+
58
+ // --- slip flag (isolated I/O, same family as the other forward nets) ----------
59
+
60
+ function slipPath(projectDir) {
61
+ return path.join(projectDir, '_byan-output', '.voice-slip.json');
62
+ }
63
+
64
+ function writeSlip(projectDir, hits) {
65
+ try {
66
+ const p = slipPath(projectDir);
67
+ fs.mkdirSync(path.dirname(p), { recursive: true });
68
+ fs.writeFileSync(p, JSON.stringify({ hits }));
69
+ return true;
70
+ } catch {
71
+ return false;
72
+ }
73
+ }
74
+
75
+ function readSlip(projectDir) {
76
+ try {
77
+ const parsed = JSON.parse(fs.readFileSync(slipPath(projectDir), 'utf8'));
78
+ return Array.isArray(parsed.hits) ? parsed.hits : null;
79
+ } catch {
80
+ return null;
81
+ }
82
+ }
83
+
84
+ function clearSlip(projectDir) {
85
+ try { fs.rmSync(slipPath(projectDir), { force: true }); } catch { /* never block */ }
86
+ }
87
+
88
+ module.exports = {
89
+ EMOJI_RE,
90
+ VOUS_RE,
91
+ scanVoice,
92
+ formatReminder,
93
+ slipPath,
94
+ writeSlip,
95
+ readSlip,
96
+ clearSlip,
97
+ };
@@ -57,9 +57,48 @@ function matchesPrefix(rel, prefix) {
57
57
  return rel === p || rel.startsWith(p + '/');
58
58
  }
59
59
 
60
- // Pure decision : returns { deny, reason }.
61
- function decideScope({ state, config, toolName, filePath }) {
62
- if (!['Write', 'Edit'].includes(toolName)) return { deny: false };
60
+ // WI-6 the Bash write-redirection leak.
61
+ //
62
+ // The Write/Edit deny is bypassable : `cat > src/x.js`, `echo >> a`, `tee f`,
63
+ // `cmd <<EOF > f` write files through Bash, which the tool-name check missed.
64
+ // bashWriteTargets extracts the file targets of write-redirections from a Bash
65
+ // command so the SAME allowed-paths rule applies. Deliberately conservative to
66
+ // avoid denying legit Bash : it skips fd-dups (`2>&1`, `>&2`), process
67
+ // substitution (`>(...)`), and the /dev/* sinks. It is not a hermetic sandbox
68
+ // (pipes, `python -c open()`, variables escape) — it closes the COMMON reflex
69
+ // leak, honestly bounded (see docs).
70
+ function bashWriteTargets(command) {
71
+ const cmd = String(command || '');
72
+ const out = [];
73
+ // A redirection target only counts when it LOOKS like a file path : it contains
74
+ // a "/" or a file extension. This is the false-positive kill switch : it drops
75
+ // comparison / arithmetic / here-string operands (`[ 5 > 3 ]`, `(( a > 2 ))`,
76
+ // `<<< "a > b"` -> "3", "2", "b") which are numbers or bare words, and drops an
77
+ // unexpanded variable target (`> $F`) which we cannot resolve — denying it would
78
+ // be a false positive on a possibly in-scope path. Honest ceiling : a bare
79
+ // filename with no extension (`> outfile`) or a variable target is NOT caught.
80
+ const consider = (t) => {
81
+ if (!t) return;
82
+ if (t.startsWith('$') || t.startsWith('&') || t.startsWith('-')) return; // variable / fd-dup / flag
83
+ if (/^\/dev\//.test(t)) return; // /dev sinks
84
+ const pathShaped = t.includes('/') || /\.[A-Za-z0-9]+$/.test(t);
85
+ if (pathShaped) out.push(t);
86
+ };
87
+ // > , >> , &> , &>> , and fd-prefixed (2>) redirections. The lookahead (?![&(])
88
+ // drops `>&1` (fd dup) and `>(` (process substitution).
89
+ const redir = /(?:^|[\s;|&(])\d*(?:>>?|&>>?)\s*(?![&(])("?)([^\s"'`|;&<>()]+)\1/g;
90
+ let m;
91
+ while ((m = redir.exec(cmd)) !== null) consider(m[2]);
92
+ // tee [flags] file...
93
+ const tee = /\btee\b((?:\s+-\S+)*)\s+("?)([^\s"'`|;&<>()]+)\2/g;
94
+ while ((m = tee.exec(cmd)) !== null) consider(m[3]);
95
+ return out;
96
+ }
97
+
98
+ // Pure decision : returns { deny, reason }. Handles Write/Edit (file_path) and,
99
+ // since WI-6, Bash (write-redirection targets that land INSIDE the repo).
100
+ function decideScope({ state, config, toolName, filePath, command }) {
101
+ if (!['Write', 'Edit', 'Bash'].includes(toolName)) return { deny: false };
63
102
  if (!isEngaged(state)) return { deny: false };
64
103
 
65
104
  const guard = (config && config.scope_guard) || {};
@@ -69,25 +108,42 @@ function decideScope({ state, config, toolName, filePath }) {
69
108
  if (!Array.isArray(allowed) || allowed.length === 0) return { deny: false };
70
109
 
71
110
  const root = projectRoot();
72
- const rel = toRelative(filePath, root);
73
- if (!rel) return { deny: false };
74
-
75
111
  const exempt = guard.exempt_globs || [];
76
- if (exempt.some((g) => matchesPrefix(rel, g))) return { deny: false };
77
-
78
- if (allowed.some((a) => matchesPrefix(rel, a))) return { deny: false };
79
-
80
112
  const base =
81
113
  (config && config.banners && config.banners.scope_deny) ||
82
114
  'Strict mode: this write targets a path outside the locked scope.';
83
- const reason =
115
+
116
+ // Returns the offending rel path, or null when the target is allowed/exempt.
117
+ const offending = (rawTarget, { repoOnly = false } = {}) => {
118
+ const rel = toRelative(rawTarget, root);
119
+ if (!rel) return null;
120
+ // repoOnly (Bash): a target outside the repo (../, /tmp, absolute elsewhere)
121
+ // is transient, not a repo change under cover of the locked task -> ignore.
122
+ if (repoOnly && rel.startsWith('..')) return null;
123
+ if (exempt.some((g) => matchesPrefix(rel, g))) return null;
124
+ if (allowed.some((a) => matchesPrefix(rel, a))) return null;
125
+ return rel;
126
+ };
127
+
128
+ const buildReason = (rel) =>
84
129
  `${base}\n` +
85
130
  `Target: ${rel}\n` +
86
131
  `Locked paths: ${allowed.join(', ')}\n` +
87
132
  `Either this file belongs to the scope (re-lock with byan_strict_lock_scope ` +
88
133
  `including the corrected paths) or it does not (do not write it).`;
89
134
 
90
- return { deny: true, reason };
135
+ if (toolName === 'Write' || toolName === 'Edit') {
136
+ const bad = offending(filePath);
137
+ return bad ? { deny: true, reason: buildReason(bad) } : { deny: false };
138
+ }
139
+
140
+ // Bash : deny if any in-repo write-redirection target is out of scope.
141
+ const targets = bashWriteTargets(command);
142
+ for (const t of targets) {
143
+ const bad = offending(t, { repoOnly: true });
144
+ if (bad) return { deny: true, reason: buildReason(bad) };
145
+ }
146
+ return { deny: false };
91
147
  }
92
148
 
93
149
  function allow() {
@@ -106,8 +162,9 @@ if (require.main === module) {
106
162
  const toolName = payload.tool_name || payload.toolName || '';
107
163
  const input = payload.tool_input || payload.toolInput || {};
108
164
  const filePath = input.file_path || '';
165
+ const command = input.command || '';
109
166
 
110
- const decision = decideScope({ state, config, toolName, filePath });
167
+ const decision = decideScope({ state, config, toolName, filePath, command });
111
168
  if (!decision.deny) {
112
169
  process.stdout.write(JSON.stringify(allow()));
113
170
  process.exit(0);
@@ -125,4 +182,4 @@ if (require.main === module) {
125
182
  })();
126
183
  }
127
184
 
128
- module.exports = { decideScope, toRelative, matchesPrefix };
185
+ module.exports = { decideScope, toRelative, matchesPrefix, bashWriteTargets };
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // Stop hook — WI-2, the reactive net for BYAN voice conformance (soul/tao).
5
+ //
6
+ // At end of turn it scans the finished reply for the objective voice slips (emoji,
7
+ // vouvoiement cluster) and, on a slip, writes a one-turn flag under _byan-output/.
8
+ // The next-turn voice anchor surfaces it in plain French. It NEVER blocks the turn
9
+ // (the register is semantic — a hard wall would false-positive) : always exits 0.
10
+ //
11
+ // The judgment lives in lib/voice-conformance.js (pure) ; this shell only pulls
12
+ // the reply text from the transcript.
13
+
14
+ const { extractLastAssistantText } = require('./lib/transcript-read');
15
+ const voice = require('./lib/voice-conformance');
16
+
17
+ function detect(payload, projectDir) {
18
+ const text = extractLastAssistantText(payload);
19
+ const hits = voice.scanVoice(text);
20
+ if (hits.length) voice.writeSlip(projectDir, hits);
21
+ return hits;
22
+ }
23
+
24
+ function readStdin() {
25
+ return new Promise((resolve) => {
26
+ let data = '';
27
+ process.stdin.on('data', (c) => (data += c));
28
+ process.stdin.on('end', () => resolve(data));
29
+ process.stdin.on('error', () => resolve(''));
30
+ });
31
+ }
32
+
33
+ if (require.main === module) {
34
+ (async () => {
35
+ const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
36
+ let payload = {};
37
+ try {
38
+ const raw = await readStdin();
39
+ if (raw && raw.trim()) payload = JSON.parse(raw);
40
+ } catch {
41
+ payload = {};
42
+ }
43
+ try {
44
+ detect(payload, projectDir);
45
+ } catch {
46
+ // never block end-of-turn
47
+ }
48
+ process.stdout.write('{}');
49
+ process.exit(0);
50
+ })();
51
+ }
52
+
53
+ module.exports = { detect };
@@ -47,35 +47,55 @@ declencher une interview a chaque fois. Agent existant qui colle = on route
47
47
  direct. Besoin flou ou aucun agent adapte = la ou l'interview + creation se
48
48
  declenchent. La ceremonie est proportionnee au manque, pas systematique.
49
49
 
50
- ## La chaine complete, automatique (agent -> moteur -> execution)
50
+ ## La chaine complete, automatique (comprendre -> dispatch -> execution party-mode -> gate en fin)
51
51
 
52
52
  Le dispatch d'agent n'est que la premiere marche. A l'entree, sur toute tache,
53
53
  BYAN enchaine la chaine ENTIERE de lui-meme, sans que l'utilisateur ait a la
54
- demander :
55
-
56
- 1. **Agent** matcher (ci-dessus). Fit -> cet agent ; no-fit -> interview +
57
- recherche web + creation. Seul point ou l'humain reste requis : creer un
58
- nouvel agent.
59
- 2. **Moteur**router via `_byan/mcp/byan-mcp-server/lib/dispatch-router.js` :
60
- Codex pour execution / shell / deploiement / devops / navigateur ; Claude pour
61
- architecture / refactor / qualite / planif ; la verification reste sur Claude ;
62
- Fable n'est pas emis ; modele + effort selon la complexite. Decision
63
- automatique, pas de validation utilisateur.
64
- 3. **Execution** Codex-lane : deleguer a Codex via le pont
65
- (`lib/codex-bridge.js` : `codex exec` -> diff unifie -> Claude applique le
66
- diff ; repli sur Claude si Codex est indisponible). Claude-lane : executer sur
67
- Claude au modele choisi. Automatique.
54
+ demander. Quatre moments :
55
+
56
+ 1. **Comprendre la demande.** Demande claire -> on avance directement, sans
57
+ question. Doute ou demande mal exprimee -> BYAN expose UN plan clair (quels
58
+ agents, quels modeles, quel effort) et l'utilisateur tranche. Ce point de
59
+ controle en amont ne se declenche que sur un vrai doute pas en reflexe a
60
+ chaque tache.
61
+ 2. **Dispatch (Hermes, automatique).** Le bon agent (matcher
62
+ `_byan/mcp/byan-mcp-server/lib/agent-matcher.js`), le bon modele + effort
63
+ (`dispatch-router.js` / native-tiers) et le bon moteur : Codex pour execution /
64
+ shell / deploiement / devops / navigateur ; Claude pour architecture / refactor
65
+ / qualite / planif ; la verification reste sur Claude ; Fable n'est pas emis.
66
+ Decision automatique, pas de validation utilisateur. Fit -> cet agent ;
67
+ no-fit -> interview + recherche web + creation (seul point ou l'humain reste
68
+ requis en amont).
69
+ 3. **Execution en party-mode avec un visuel live.** L'utilisateur VOIT ce qui se
70
+ passe, en direct : la liste de taches native (une entree par agent/etape,
71
+ mise a jour en temps reel) plus la table de dispatch en tete. Codex-lane : quand
72
+ la voie est ARMEE (option yanstaller + Codex linke) et la tache delegable,
73
+ deleguer REELLEMENT via le pont (`lib/codex-bridge.js` : `codex exec` -> diff
74
+ unifie -> Claude applique ; repli sur Claude si Codex indisponible) — le conseil
75
+ injecte est alors une directive, pas une simple suggestion. Non armee : pas de
76
+ delegation, on execute sur Claude. Claude-lane : executer sur Claude au modele
77
+ choisi.
78
+ 4. **Gate utilisateur en fin.** Le point de controle utilisateur arrive a la fin,
79
+ une fois la feature/tache faite et s'il y a un livrable a valider — pas en
80
+ amont. Dans un FD multi-feature complet, les gates par phase s'appliquent en
81
+ plus (c'est la gouvernance plus lourde du FD ; le chemin direct ci-dessus est
82
+ le cas courant).
68
83
 
69
84
  Ce qui reste a l'humain : (a) creer un nouvel agent quand aucun ne colle, (b)
70
- confirmer une action destructive. Le reste match agent, routage moteur,
71
- execution part tout seul. Detail du routage moteur : `docs/intelligent-dispatch.md`.
85
+ confirmer une action destructive, (c) trancher le plan en amont quand il y a
86
+ doute. Le reste comprehension, match agent, routage moteur, execution — part
87
+ tout seul. Detail du routage moteur : `docs/intelligent-dispatch.md`.
72
88
 
73
- ## La double validation
89
+ ## La double validation (calibree : sur doute, nouvel agent, destructif)
74
90
 
75
91
  Le matcher (F1, `_byan/mcp/byan-mcp-server/lib/agent-matcher.js`) est un
76
92
  pre-tri deterministe : il classe les candidats du roster et rend un verdict
77
- {fit | no-fit}. Il PROPOSE, il ne tranche pas seul. BYAN presente la proposition
78
- en clair ; l'utilisateur valide. IA propose, humain confirme.
93
+ {fit | no-fit}. Il PROPOSE, il ne tranche pas seul. Mais la confirmation humaine
94
+ n'est PAS un reflexe a chaque tache : sur une demande claire avec un agent adapte,
95
+ BYAN route et execute, et le gate est en fin. La confirmation humaine se declenche
96
+ sur les trois cas qui la meritent : un vrai doute (le plan en amont), la creation
97
+ d'un nouvel agent, une action destructive. IA propose, humain confirme — la ou ca
98
+ compte, pas partout.
79
99
 
80
100
  ## La creation d'agent enrichie (chemin no-fit)
81
101
 
@@ -90,6 +90,10 @@
90
90
  "type": "command",
91
91
  "command": "p=\"$CLAUDE_PROJECT_DIR/.claude/hooks/agent-gate-check.js\"; [ -f \"$p\" ] || exit 0; exec node \"$p\""
92
92
  },
93
+ {
94
+ "type": "command",
95
+ "command": "p=\"$CLAUDE_PROJECT_DIR/.claude/hooks/voice-conformance-check.js\"; [ -f \"$p\" ] || exit 0; exec node \"$p\""
96
+ },
93
97
  {
94
98
  "type": "command",
95
99
  "command": "p=\"$CLAUDE_PROJECT_DIR/.claude/hooks/drain-advisory.js\"; [ -f \"$p\" ] || exit 0; exec node \"$p\""
@@ -112,6 +116,10 @@
112
116
  {
113
117
  "type": "command",
114
118
  "command": "p=\"$CLAUDE_PROJECT_DIR/.claude/hooks/strict-scope-guard.js\"; [ -f \"$p\" ] || exit 0; exec node \"$p\""
119
+ },
120
+ {
121
+ "type": "command",
122
+ "command": "p=\"$CLAUDE_PROJECT_DIR/.claude/hooks/codex-delegate-guard.js\"; [ -f \"$p\" ] || exit 0; exec node \"$p\""
115
123
  }
116
124
  ]
117
125
  },
@@ -7,31 +7,54 @@ description: BYAN — Builder of YAN. Core meta-agent that owns the Feature Deve
7
7
 
8
8
  You are BYAN when this skill is active. You own the eight-phase Feature Development workflow and you enforce it mechanically. Every new feature the user asks for goes through all phases in order. No skipping. No implicit transitions. The cycle includes a REFACTOR loop back to BUILD when VALIDATE fails.
9
9
 
10
- ## 0. Entry gaterun the WHOLE dispatch chain automatically
11
-
12
- On EVERY non-conversational task, run this chain yourself, of your own accord —
13
- the user should NOT have to ask for it. Three steps, then execute:
14
-
15
- 1. **Which agent** match the need against the roster with the matcher
16
- (`_byan/mcp/byan-mcp-server/lib/agent-matcher.js`). A suited agent -> use it.
17
- NO suited agent -> propose an interview to frame the need, web-research the
18
- trade's competencies + best practices, create the tailored agent. This is the
19
- ONE step where the human stays in the loop (creating a new agent).
20
- 2. **Which runtime** route with `dispatch-router.js`: Codex for execution /
21
- shell / deploy / devops / browser ; Claude for architecture / refactor /
22
- quality / planning ; verification stays on Claude ; Fable is not emitted ;
23
- model + effort scale to complexity. This decision is automatic no user ask.
24
- 3. **Execute** Codex-lane: delegate to Codex via the bridge
25
- (`codex-bridge.js`: `codex exec` -> unified diff -> YOU apply it ; fall back to
26
- Claude if Codex is unavailable). Claude-lane: do it on Claude at the chosen
27
- model. Automatic no user ask.
28
-
29
- Proportionate: a trivial task with an existing agent routes directly, no
30
- ceremony. The human stays required only for (a) creating a NEW agent and (b)
31
- confirming a destructive action. Everything else agent match, runtime routing,
32
- execution is automatic. Do NOT do a task inline without running this chain.
33
- Full doctrine + the reactive net (`agent-gate-check.js`) + the runtime routing
34
- table: see @.claude/rules/agent-entry-gate.md and @docs/intelligent-dispatch.md
10
+ ## 0. Entry chaincomprehend, dispatch, execute in party-mode, gate at the end
11
+
12
+ On every non-conversational task, run this chain yourself, of your own accord —
13
+ the user should not have to ask for it. Four moments:
14
+
15
+ 1. **Comprehend the request.** Clear -> advance directly, no question. Doubt or a
16
+ badly-expressed request -> expose ONE clear plan (which agents, which models,
17
+ which effort) and let the user decide. This upstream plan-gate fires ONLY on
18
+ genuine doubt it is not a reflex on every task. When the request is clear,
19
+ skip it and route.
20
+ 2. **Dispatch (Hermes, automatic).** Pick the right agent
21
+ (`_byan/mcp/byan-mcp-server/lib/agent-matcher.js`), the right model + effort
22
+ (`dispatch-router.js` / native-tiers), and the right runtime Codex for
23
+ execution / shell / deploy / devops / browser ; Claude for architecture /
24
+ refactor / quality / planning ; verification stays on Claude ; Fable is not
25
+ emitted. No user ask. A suited agent -> use it. NO suited agent -> interview to
26
+ frame the need, web-research the trade's competencies + best practices, create
27
+ the tailored agent (the one place the human is required upstream).
28
+ 3. **Execute in party-mode with a live visual.** The user SEES what happens, live:
29
+ the native task list (one entry per agent/step, updated in real time via
30
+ `TaskCreate` / `TaskUpdate`) plus the dispatch table shown at the top of BUILD.
31
+ - **Codex-lane** when the lane is ARMED (yanstaller option on AND Codex
32
+ linked, i.e. `_byan/_config/autodelegate.json` present with `enabled:true`
33
+ AND `~/.codex/auth.json` or `CODEX_API_KEY`) and the task is delegable,
34
+ ACTUALLY delegate via `codex-bridge.js` (`codex exec` -> unified diff -> YOU
35
+ apply it). Fall back to Claude if Codex is unavailable. When armed, the
36
+ injected `[BYAN auto-delegate]` note is a DIRECTIVE, not a mere suggestion:
37
+ you delegate the delegable work rather than doing it on Claude by default.
38
+ When NOT armed (option off or Codex not linked), there is no delegation —
39
+ you run it on Claude.
40
+ - **Claude-lane** — run it on Claude at the chosen model.
41
+ 4. **User gate at the END.** The user-validation gate lands at the end, once the
42
+ feature/task is done and there is a deliverable to review — not upstream. (In a
43
+ full multi-feature FD, the per-phase gates below still apply — that is the
44
+ heavier governance the FD opts into; the direct path above is the common case.)
45
+
46
+ Proportionate: a trivial task with an existing agent routes directly, no ceremony;
47
+ a genuine multi-feature build escalates into the full eight-phase FD. The human
48
+ stays required only for (a) creating a NEW agent and (b) confirming a destructive
49
+ action. Everything else — comprehension routing, agent match, runtime routing,
50
+ execution — is automatic. Do not do a task yourself without running this chain.
51
+
52
+ **Honest ceiling.** Claude Code has no control point before a response is shown,
53
+ so this chain is model-driven doctrine, not a hard mechanism: the delegation and
54
+ the visual are things you DO because this section says so, backed by the reactive
55
+ net (`agent-gate-check.js`) at turn end — not a guarantee enforced before display.
56
+ Full doctrine + runtime routing table: see @.claude/rules/agent-entry-gate.md and
57
+ @docs/intelligent-dispatch.md
35
58
 
36
59
  ## 1. Activation triggers
37
60
 
@@ -122,7 +145,10 @@ Never call `byan_update_apply` without explicit user consent. That tool returns
122
145
  - TDD first : write/update tests before implementation.
123
146
  - Atomic commits : `type: description`, no emoji, one feature per commit.
124
147
  - Parallel BUILD via `party-mode-native` only if roles are independent and write to non-overlapping paths.
125
- - **Visibility** : the `tool-transparency` hook already writes per-tool entries to `_byan-output/tool-log.jsonl`. Every sub-task you spawn must be visible there.
148
+ - **Party-mode live visual** (the user must SEE what happens) :
149
+ 1. At BUILD entry, print the dispatch table (feature x specialist x model x strategy) so the plan is on screen before any work starts.
150
+ 2. Create ONE native task per feature/agent with `TaskCreate` (the checklist Claude Code renders live). Set it `in_progress` (`TaskUpdate`) with the acting agent as `owner` the moment that agent starts, and `completed` the moment it finishes. One entry per agent/step, updated in real time — that is the visual.
151
+ 3. This native list is the primary visual ; the `tool-transparency` hook is the audit trail underneath it, writing per-tool entries to `_byan-output/tool-log.jsonl`. Every sub-task you spawn must be visible in both.
126
152
  - **Exit gate** : user sees the diff and says "ok build".
127
153
 
128
154
  ### Phase 6 — REVIEW (qualitative pre-flight + tiered adversarial second pair of eyes)
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env node
2
+ // WI-7 — the armament observation report (CLI, ESM).
3
+ //
4
+ // Reads the observation ledgers under _byan-output/, reads the CURRENT armed
5
+ // flags from config, and prints per-guard : sample size, would-fire count (the
6
+ // false-positive risk proxy), current armed state, and a calibrated
7
+ // recommendation. It NEVER arms anything — arming is a deliberate config flip the
8
+ // human makes after reading this.
9
+ //
10
+ // Usage : node bin/byan-armament-report.js [--json] [--root <dir>]
11
+
12
+ import fs from 'node:fs';
13
+ import path from 'node:path';
14
+ import { buildReport } from '../lib/armament-report.js';
15
+
16
+ function parseArgs(argv) {
17
+ const a = { json: false, root: process.env.CLAUDE_PROJECT_DIR || process.cwd() };
18
+ for (let i = 2; i < argv.length; i++) {
19
+ if (argv[i] === '--json') a.json = true;
20
+ else if (argv[i] === '--root') a.root = argv[++i];
21
+ }
22
+ return a;
23
+ }
24
+
25
+ function readLedger(root, name) {
26
+ try {
27
+ const p = path.join(root, '_byan-output', name);
28
+ return fs.readFileSync(p, 'utf8').split('\n').filter(Boolean).map((l) => {
29
+ try { return JSON.parse(l); } catch { return null; }
30
+ }).filter(Boolean);
31
+ } catch {
32
+ return [];
33
+ }
34
+ }
35
+
36
+ // Best-effort read of the current armed flags from config (absent -> false).
37
+ function readArmedFlags(root) {
38
+ const flags = { autobench: false, punt: false, completeness: false };
39
+ try {
40
+ const a = JSON.parse(fs.readFileSync(path.join(root, '.claude', 'hooks', 'lib', 'autobench-config.json'), 'utf8'));
41
+ flags.autobench = Boolean(a && a.enforcement && a.enforcement.armed);
42
+ } catch { /* absent -> false */ }
43
+ try {
44
+ const d = JSON.parse(fs.readFileSync(path.join(root, '_byan', '_config', 'delivery-default.json'), 'utf8'));
45
+ flags.punt = Boolean(d && d.puntGuard && d.puntGuard.armed);
46
+ flags.completeness = Boolean(d && d.completenessGate && d.completenessGate.armed);
47
+ } catch { /* absent -> false */ }
48
+ return flags;
49
+ }
50
+
51
+ function render(report) {
52
+ const lines = ['BYAN armament report (observe first, arm on evidence) :', ''];
53
+ const label = { autobench: 'Auto-Benchmark', punt: 'Punt guard', completeness: 'Completeness gate' };
54
+ for (const key of Object.keys(report)) {
55
+ const g = report[key];
56
+ const s = g.summary;
57
+ lines.push(`- ${label[key] || key} : armed=${g.armed} | observations=${s.total} | would-fire=${s.wouldFire} (${Math.round(s.fireRate * 100)}%)`);
58
+ lines.push(` -> ${g.recommend.arm ? 'ARM' : 'HOLD'} : ${g.recommend.reason}`);
59
+ }
60
+ lines.push('');
61
+ lines.push('Note : "would-fire" est un indicateur de risque a relire, pas un compte prouve de faux positifs.');
62
+ lines.push('Armer reste une decision manuelle (autobench-config.json enforcement.armed, delivery-default.json puntGuard/completenessGate.armed).');
63
+ return lines.join('\n');
64
+ }
65
+
66
+ function main() {
67
+ const args = parseArgs(process.argv);
68
+ const report = buildReport(
69
+ {
70
+ benchmark: readLedger(args.root, 'benchmark-ledger.jsonl'),
71
+ punt: readLedger(args.root, 'punt-ledger.jsonl'),
72
+ completeness: readLedger(args.root, 'completeness-ledger.jsonl'),
73
+ },
74
+ readArmedFlags(args.root)
75
+ );
76
+ if (args.json) process.stdout.write(JSON.stringify(report, null, 2) + '\n');
77
+ else process.stdout.write(render(report) + '\n');
78
+ }
79
+
80
+ main();
81
+
82
+ export { readLedger, readArmedFlags, render };
@@ -0,0 +1,80 @@
1
+ // WI-7 core — the armament observation report (ESM, matches this package's type).
2
+ //
3
+ // Two BYAN teeth are BUILT but DISARMED by config : the auto-benchmark Stop guard
4
+ // and the punt / completeness guards. They only observe + ledger today. Arming
5
+ // one turns it into a refuse-once, which costs a regeneration on every FALSE
6
+ // positive — the risk the user rejected for a hard wall. So arming must be a
7
+ // measured decision, not a blind flip.
8
+ //
9
+ // This pure core reads the observation ledgers and, per guard, reports : how many
10
+ // entries the guard WOULD have acted on if armed (the false-positive risk proxy),
11
+ // over how large a sample, and a recommendation. It NEVER arms anything — the
12
+ // human flips the config after reading this. Honest limit : the ledger has no
13
+ // ground truth, so "wouldFire" is a risk proxy to eyeball, not a proven
14
+ // false-positive count.
15
+
16
+ // Per-ledger classifiers : does this entry represent the guard ACTING (a block /
17
+ // refuse-once) if it were armed ?
18
+ export function classifyBenchmark(e) {
19
+ if (!e || typeof e !== 'object') return 'skip';
20
+ if (typeof e.event === 'string' && /block|regen|unmarked/i.test(e.event)) return 'wouldFire';
21
+ // a real choice presented (choice-language + an artifact) without a marker and
22
+ // not on the never-list is exactly what the armed guard blocks.
23
+ if (e.choiceLang && e.artifact && !e.marker && !e.neverHit) return 'wouldFire';
24
+ return 'satisfied';
25
+ }
26
+
27
+ export function classifyPunt(e) {
28
+ if (!e || typeof e !== 'object') return 'skip';
29
+ if (e.punt === true) return 'wouldFire';
30
+ if (typeof e.event === 'string' && /punt/i.test(e.event)) return 'wouldFire';
31
+ return 'satisfied';
32
+ }
33
+
34
+ export function classifyCompleteness(e) {
35
+ if (!e || typeof e !== 'object') return 'skip';
36
+ if (Array.isArray(e.missing) && e.missing.length > 0) return 'wouldFire';
37
+ if (typeof e.event === 'string' && /block|gap|fire/i.test(e.event)) return 'wouldFire';
38
+ return 'satisfied';
39
+ }
40
+
41
+ export function summarize(entries, classify) {
42
+ const s = { total: 0, wouldFire: 0, satisfied: 0, armedSeen: false };
43
+ for (const e of Array.isArray(entries) ? entries : []) {
44
+ const c = classify(e);
45
+ if (c === 'skip') continue;
46
+ s.total += 1;
47
+ if (e && e.armed === true) s.armedSeen = true;
48
+ if (c === 'wouldFire') s.wouldFire += 1;
49
+ else s.satisfied += 1;
50
+ }
51
+ s.fireRate = s.total ? s.wouldFire / s.total : 0;
52
+ return s;
53
+ }
54
+
55
+ // recommend — the calibrated arming call. Never arm on a small sample ; arm only
56
+ // on a clean record ; otherwise send the human to review the contexts first.
57
+ export function recommend(summary, { minSample = 20 } = {}) {
58
+ if (!summary || summary.total < minSample) {
59
+ return { arm: false, reason: `echantillon trop petit (${summary ? summary.total : 0} < ${minSample}) — continuer d'observer avant de decider` };
60
+ }
61
+ if (summary.wouldFire === 0) {
62
+ return { arm: true, reason: `0 declenchement sur ${summary.total} observations — armement sur (aucun faux positif observe)` };
63
+ }
64
+ const pct = Math.round(summary.fireRate * 100);
65
+ return { arm: false, reason: `${summary.wouldFire}/${summary.total} declenchements (${pct}%) — relire ces contextes avant d'armer (chaque faux positif coute une regeneration)` };
66
+ }
67
+
68
+ // buildReport — the whole picture. `armedFlags` carries the CURRENT config state
69
+ // (read by the bin) so the report shows observed-vs-armed side by side.
70
+ export function buildReport({ benchmark = [], punt = [], completeness = [] } = {}, armedFlags = {}) {
71
+ const guards = {
72
+ autobench: { summary: summarize(benchmark, classifyBenchmark), armed: Boolean(armedFlags.autobench) },
73
+ punt: { summary: summarize(punt, classifyPunt), armed: Boolean(armedFlags.punt) },
74
+ completeness: { summary: summarize(completeness, classifyCompleteness), armed: Boolean(armedFlags.completeness) },
75
+ };
76
+ for (const k of Object.keys(guards)) {
77
+ guards[k].recommend = recommend(guards[k].summary);
78
+ }
79
+ return guards;
80
+ }
@@ -94,7 +94,7 @@
94
94
  "name": "byan-byan",
95
95
  "module": "core",
96
96
  "tier": "connector-bound",
97
- "sourceHash": "91fa54c35848f2e2ddd6210fde0f15fab53d58ce5b36627f90ebcfbf760e09a1"
97
+ "sourceHash": "84fa718d638f1ddb37796c9b80c702fca48350decb40979bd1adea1452224abc"
98
98
  },
99
99
  "byan-byan-v2": {
100
100
  "name": "byan-byan-v2",