create-byan-agent 2.49.0 → 2.52.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.
- package/CHANGELOG.md +78 -0
- package/install/package.json +1 -1
- package/install/templates/.claude/hooks/agent-gate-check.js +14 -2
- package/install/templates/.claude/hooks/codex-delegate-guard.js +137 -0
- package/install/templates/.claude/hooks/inject-voice-anchor.js +25 -0
- package/install/templates/.claude/hooks/lib/agent-gate.js +95 -5
- package/install/templates/.claude/hooks/lib/autodelegate-decision.js +11 -13
- package/install/templates/.claude/hooks/lib/codex-delegate-gate.js +130 -0
- package/install/templates/.claude/hooks/lib/fact-check-core.js +34 -2
- package/install/templates/.claude/hooks/lib/voice-conformance.js +97 -0
- package/install/templates/.claude/hooks/strict-scope-guard.js +71 -14
- package/install/templates/.claude/hooks/voice-conformance-check.js +53 -0
- package/install/templates/.claude/rules/native-workflows.md +17 -7
- package/install/templates/.claude/settings.json +8 -0
- package/install/templates/.claude/skills/byan-byan/SKILL.md +16 -10
- package/install/templates/_byan/mcp/byan-mcp-server/bin/byan-armament-report.js +82 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/armament-report.js +80 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/dispatch-router.js +48 -30
- package/install/templates/_byan/mcp/byan-mcp-server/lib/native-tiers.js +25 -6
- package/install/templates/_byan/mcp/byan-mcp-server/lib/tier-script.js +8 -4
- package/install/templates/_byan/mcp/byan-mcp-server/lib/workflows-lint.js +6 -2
- package/install/templates/_byan/mcp/byan-mcp-server/skill-bundles-manifest.json +1 -1
- package/install/templates/dist/skill-bundles/byan-byan.zip +0 -0
- package/install/templates/docs/codex-auto-delegation.md +51 -25
- package/install/templates/docs/intelligent-dispatch.md +9 -5
- package/install/templates/docs/native-workflows-contract.md +9 -4
- package/package.json +1 -1
|
@@ -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-
|
|
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(
|
|
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
|
|
@@ -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
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 };
|
|
@@ -26,7 +26,8 @@ A native workflow script mutates FD/strict state only through the `byan_fd_*` /
|
|
|
26
26
|
|
|
27
27
|
Each `agent()` leaf runs on the session model unless the call sets `opts.model`.
|
|
28
28
|
The tiering decision lives in one place — `_byan/mcp/byan-mcp-server/lib/native-tiers.js`
|
|
29
|
-
(tier vocabulary, leaf classifier, model map). Three tiers
|
|
29
|
+
(tier vocabulary, leaf classifier, model map). Three AUTO-routed tiers, plus an
|
|
30
|
+
explicit UP-TIER an author may pin by complexity (v3):
|
|
30
31
|
|
|
31
32
|
- **cheap (`model: 'haiku'`)** — a pure EXPLORATION leaf (read/load/parse/detect).
|
|
32
33
|
- **balanced (`model: 'sonnet'`)** — two classes land here. (1) MECHANICAL
|
|
@@ -40,16 +41,25 @@ The tiering decision lives in one place — `_byan/mcp/byan-mcp-server/lib/nativ
|
|
|
40
41
|
leaf may carry `model: 'sonnet'` (its tier) but not `haiku` (`analysis-below-tier`).
|
|
41
42
|
Semantic/adversarial VERIFICATION is NOT analysis and stays deep.
|
|
42
43
|
- **deep (OMIT `opts.model`)** — implementation + verification leaves inherit the
|
|
43
|
-
session model (no pin-up
|
|
44
|
-
deep with the `deep-` label prefix
|
|
45
|
-
`mech-` opt-in.
|
|
44
|
+
session model (the cost-safe default: no AUTO pin-up). A genuinely frontier
|
|
45
|
+
ANALYSIS leaf opts back to deep with the `deep-` label prefix
|
|
46
|
+
(`deep-assess-architecture`), the mirror of the `mech-` opt-in.
|
|
47
|
+
- **up-tier (`model: 'opus'` / `model: 'fable'`)** — an EXPLICIT authoring choice
|
|
48
|
+
to raise a genuinely complex leaf ABOVE the inherited tier (v3). `opus` for a
|
|
49
|
+
hard leaf, `fable` as the last-resort top-reasoning model for extreme complexity
|
|
50
|
+
(~2x Opus price). This is the workflow-leaf mirror of the dispatch-router
|
|
51
|
+
complexity ladder (haiku -> sonnet -> opus -> fable). Auto-routing does not pick
|
|
52
|
+
it — the author writes it deliberately. The anti-downgrade floor does not apply
|
|
53
|
+
upward, so the linter allows an up-tier pin on any leaf (it costs more, it does
|
|
54
|
+
not regress quality). The old blanket "no pin-up / no Fable" ban is lifted.
|
|
46
55
|
|
|
47
56
|
The linter splits the two directions:
|
|
48
57
|
|
|
49
|
-
- **Floor (HARD, blocks the commit)** — a
|
|
50
|
-
|
|
58
|
+
- **Floor (HARD, blocks the commit)** — a DOWNGRADE model (haiku/sonnet) on a
|
|
59
|
+
PROTECTED leaf, or a half-applied `mech-` opt-in, is a contract violation
|
|
51
60
|
(`modelRoutingViolations` + `mechanicalLabelViolations`). This is the STRICT-2
|
|
52
|
-
No Downgrade net.
|
|
61
|
+
No Downgrade net. An UP-TIER pin (opus/fable) is not a violation — the floor
|
|
62
|
+
guards downward only.
|
|
53
63
|
- **Ceiling (ADVISORY, non-blocking)** — an exploration- OR analysis-labelled leaf
|
|
54
64
|
that runs deep is *reported* (`byan-lint-workflows.js --advise`:
|
|
55
65
|
`untiered-exploration` -> `model: 'haiku'`, `untiered-analysis` -> `model: 'sonnet'`),
|
|
@@ -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
|
},
|
|
@@ -21,8 +21,10 @@ the user should not have to ask for it. Four moments:
|
|
|
21
21
|
(`_byan/mcp/byan-mcp-server/lib/agent-matcher.js`), the right model + effort
|
|
22
22
|
(`dispatch-router.js` / native-tiers), and the right runtime — Codex for
|
|
23
23
|
execution / shell / deploy / devops / browser ; Claude for architecture /
|
|
24
|
-
refactor / quality / planning ; verification stays on Claude
|
|
25
|
-
|
|
24
|
+
refactor / quality / planning ; verification stays on Claude. The Claude model
|
|
25
|
+
scales on a four-rung complexity ladder (haiku -> sonnet -> opus -> fable, fable
|
|
26
|
+
the extreme last resort) ; Fable is refused on the Codex path only. No user ask.
|
|
27
|
+
A suited agent -> use it. NO suited agent -> interview to
|
|
26
28
|
frame the need, web-research the trade's competencies + best practices, create
|
|
27
29
|
the tailored agent (the one place the human is required upstream).
|
|
28
30
|
3. **Execute in party-mode with a live visual.** The user SEES what happens, live:
|
|
@@ -30,13 +32,17 @@ the user should not have to ask for it. Four moments:
|
|
|
30
32
|
`TaskCreate` / `TaskUpdate`) plus the dispatch table shown at the top of BUILD.
|
|
31
33
|
- **Codex-lane** — when the lane is ARMED (yanstaller option on AND Codex
|
|
32
34
|
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,
|
|
35
|
+
AND `~/.codex/auth.json` or `CODEX_API_KEY`) and the task is delegable code,
|
|
34
36
|
ACTUALLY delegate via `codex-bridge.js` (`codex exec` -> unified diff -> YOU
|
|
35
|
-
apply it)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
apply it) or the `codex:codex-rescue` agent. This is a DIRECTIVE, enforced by
|
|
38
|
+
the `codex-delegate-guard` PreToolUse tooth : it DENIES a delegable code
|
|
39
|
+
Write/Edit that skipped Codex. The ONLY valid reasons to stay on Claude are
|
|
40
|
+
(1) Codex unavailable (auto-detected), or (2) the USER asked to stay on Claude
|
|
41
|
+
("reste sur Claude" / "sans codex", or the `.byan-codex-autodelegate/off`
|
|
42
|
+
switch). "Small script / latency / I verify anyway" is NOT valid — that is
|
|
43
|
+
exactly the self-dodge the tooth closes (option B). You cannot self-grant a
|
|
44
|
+
bypass. When NOT armed (option off or Codex not linked), there is no
|
|
45
|
+
delegation — you run it on Claude.
|
|
40
46
|
- **Claude-lane** — run it on Claude at the chosen model.
|
|
41
47
|
4. **User gate at the END.** The user-validation gate lands at the end, once the
|
|
42
48
|
feature/task is done and there is a deliverable to review — not upstream. (In a
|
|
@@ -129,9 +135,9 @@ Never call `byan_update_apply` without explicit user consent. That tool returns
|
|
|
129
135
|
- nature `exploration` (load/read/scan/list/parse/fetch...) → `haiku`
|
|
130
136
|
- nature `mechanical` (binary judgment-free checks : JSON parses, schema matches, lint passes — label prefix `mech-`, explicit opt-in only) → `sonnet`
|
|
131
137
|
- nature `implementation` / `verification` / `analysis` / unknown → deep = **inherit the session model**
|
|
132
|
-
- Keep protected work (verify/analysis/implement) off haiku/sonnet regardless of size
|
|
138
|
+
- Keep protected work (verify/analysis/implement) off haiku/sonnet regardless of size. Auto-routing does not pin up ; but an author MAY deliberately UP-TIER a genuinely complex leaf to `opus` (hard) or `fable` (extreme, last resort) — the linter allows it (the floor guards downward only). Pass an explicit `nature` to `byan_dispatch` when you know it.
|
|
133
139
|
- **Inside native workflow scripts** (`.claude/workflows/*.js` OR ad-hoc) the SAME tiering applies per `agent()` leaf via `opts.model`, enforced as a FLOOR not a ceiling on TWO nets :
|
|
134
|
-
- **Repo linter** (committed scripts, pre-commit) : `modelRoutingViolations` HARD-blocks a downgrade on a protected leaf,
|
|
140
|
+
- **Repo linter** (committed scripts, pre-commit) : `modelRoutingViolations` HARD-blocks a downgrade (haiku/sonnet) on a protected leaf, or a half-applied `mech-` opt-in (`mechanical-without-model` / `mechanical-below-tier`) ; an UP-TIER pin (opus/fable) is allowed, not blocked (the floor guards downward only) ; an exploration-labelled leaf left deep is a NON-blocking ADVISORY (`byan-lint-workflows.js --advise`), since many such leaves bear a gate/classification/exact-conversion and must stay deep — the human owns that call.
|
|
135
141
|
- **Tier gate hook** (EVERY Workflow invocation, inline or scriptPath) : `tier-script-guard.js` (PreToolUse) runs the same analysis (`lib/tier-script.js`) and DENIES ONCE when exploration/`mech-` leaves have no tier, with the exact leaf list to fix. Acknowledge deliberate deep choices with the `// BYAN-TIER: reviewed` comment marker ; an identical resubmission passes (deny-once by design, no trap). Every decision lands in `_byan-output/tier-ledger.jsonl` (the measurement basis for token gains). Escape hatch : `.byan-tier/off`.
|
|
136
142
|
- **Authoring aid** : BEFORE writing a script, call `byan_dispatch` with `{ leaves: [{ label, nature? }] }` (batch mode) to get the `opts.model` per leaf ; write `model:` only where non-null. Report with `node _byan/mcp/byan-mcp-server/bin/byan-tier-script.js <file> [--json]`.
|
|
137
143
|
- No per-leaf effort knob exists (the API exposes only `model`), so effort-by-complexity reduces to model-by-complexity.
|
|
@@ -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
|
+
}
|