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
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,84 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
+
## [2.52.0] - 2026-07-20
|
|
13
|
+
|
|
14
|
+
### Changed — Delegation Codex v3 : pression-only + obeissance au routeur (F1, F2)
|
|
15
|
+
- **F1 — la delegation ne se declenche plus que sous PRESSION budget.** La nature
|
|
16
|
+
d'une tache (code / mecanique) n'est plus un declencheur d'auto-delegation a elle
|
|
17
|
+
seule. Deleguer au Codex de l'abonnement (qui reste en dessous de Claude en
|
|
18
|
+
qualite) vaut le coup pour epargner le budget Claude, pas par tache de code. Hors
|
|
19
|
+
pression, le code tourne sur Claude sans rappel. Le seuil par defaut passe a 75 %
|
|
20
|
+
(`autodelegate-decision.js` + `codex-delegate-guard.js` alignes sur un seul
|
|
21
|
+
nombre). Sans jauge `budget`, la pression n'est pas calculee et rien n'est
|
|
22
|
+
propose (documente, non bloquant).
|
|
23
|
+
- **F2 — le garde `codex-delegate-guard` obeit au routeur.** Le blocage ne mord
|
|
24
|
+
que pour une tache que le `dispatch-router` route lui-meme vers Codex (nature
|
|
25
|
+
execution / shell / deploiement / devops / navigateur). Une tache
|
|
26
|
+
architecture / refactor / qualite / planif — et toute verification — route vers
|
|
27
|
+
Claude, donc le garde reste silencieux. Le garde recalcule router + pression
|
|
28
|
+
uniquement dans la branche ou un blocage est encore possible (les chemins
|
|
29
|
+
d'autorisation courants ne paient rien) ; les deux signaux echouent en mode
|
|
30
|
+
ouvert (signal irresolu -> on autorise, sans blocage a tort).
|
|
31
|
+
|
|
32
|
+
### Changed — Echelle de modele par complexite + Fable dernier recours (F3)
|
|
33
|
+
- **Cote routeur (`dispatch-router.claudeModelForComplexity`)** : echelle a quatre
|
|
34
|
+
rungs — `haiku` (bas) -> `sonnet` (moyen) -> `opus` (haut) -> `fable` (extreme,
|
|
35
|
+
dernier recours, ~2x le prix d'Opus). C'est une RECOMMANDATION par tache, pas un
|
|
36
|
+
changement du modele de session en cours (Claude Code n'expose pas ce levier).
|
|
37
|
+
- **Cote leaves de workflow (`native-tiers` + linter)** : un up-tier explicite
|
|
38
|
+
(`opus` / `fable`) devient un choix d'auteur AUTORISE sur n'importe quel leaf,
|
|
39
|
+
miroir de l'echelle du routeur. Le plancher anti-downgrade est conserve (haiku /
|
|
40
|
+
sonnet interdits sur un leaf protege) ; seul le plafond est ouvert (le pin-up
|
|
41
|
+
n'est plus une violation). Nouveaux : `UP_TIER_MODELS`, `isUpTierModel`.
|
|
42
|
+
- **Revirement d'invariant assume** : l'ancienne ligne rouge "pas de Fable /
|
|
43
|
+
pas de pin-up" est levee cote Claude. Cote Codex elle tient (Codex ne peut pas
|
|
44
|
+
lancer un modele Claude) : `assertNoFable` garde le chemin Codex + `codex-bridge`.
|
|
45
|
+
- Doctrine alignee : `native-workflows.md`, `native-workflows-contract.md`,
|
|
46
|
+
`intelligent-dispatch.md`, `codex-auto-delegation.md`, skill `byan-byan`.
|
|
47
|
+
|
|
48
|
+
## [2.51.0] - 2026-07-20
|
|
49
|
+
|
|
50
|
+
### Changed — Delegation Codex : fermeture de l'auto-esquive (option B)
|
|
51
|
+
- Le blocage `codex-delegate-guard` ne peut plus etre contourne par BYAN lui-meme.
|
|
52
|
+
Retire : la marque de contenu auto-ecrite (`// BYAN-DELEGATE: reviewed`) et
|
|
53
|
+
l'auto-passe par re-soumission/fenetre de grace — les deux permettaient a Claude
|
|
54
|
+
de s'auto-accorder la sortie. Les seules sorties valides sont desormais : une
|
|
55
|
+
delegation Codex reelle ce tour, Codex indisponible (detecte via `codex --version`),
|
|
56
|
+
ou un opt-out HUMAIN ("reste sur claude" / "sans codex" dans la demande, ou le
|
|
57
|
+
fichier `.byan-codex-autodelegate/off`). Le motif "script court / latence / je
|
|
58
|
+
verifie" n'est plus une raison valide. Coeur pur `lib/codex-delegate-gate.js`
|
|
59
|
+
(`humanOptOutFromText`, `decideDelegateGate` durci) + `codex-delegate-guard.js`
|
|
60
|
+
(probe Codex, opt-out depuis la demande). Le rendu injecte (`autodelegate-decision`
|
|
61
|
+
renderNudge), le skill `byan-byan` et `docs/codex-auto-delegation.md` sont alignes.
|
|
62
|
+
|
|
63
|
+
### Notes
|
|
64
|
+
- Constat de session : le CLI Codex de l'abo ChatGPT est plafonne a gpt-5.5 (5.6
|
|
65
|
+
refuse). Recherche croisee : gpt-5.5 est sous le meilleur Claude sur tous les
|
|
66
|
+
domaines mesures. Donc la delegation Codex vaut pour economiser le budget Claude,
|
|
67
|
+
pas pour la qualite. Prochain chantier (FD dedie) : delegation declenchee par la
|
|
68
|
+
PRESSION budget (~75%) au lieu de par-nature ; le blocage obeit au routeur ;
|
|
69
|
+
echelle modele Claude par complexite (haiku/sonnet/opus) + Fable en dernier recours.
|
|
70
|
+
|
|
71
|
+
## [2.50.0] - 2026-07-17
|
|
72
|
+
|
|
73
|
+
### Added — BYAN souverainete (les concepts BYAN pilotent Claude, ne se font plus doubler)
|
|
74
|
+
- **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).
|
|
75
|
+
- **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).
|
|
76
|
+
- **WI-3 filet dispatch runtime** : drapeau quand un tour ecrit du code sans avoir consulte `byan_dispatch`, hors FD.
|
|
77
|
+
- **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.
|
|
78
|
+
- **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).
|
|
79
|
+
|
|
80
|
+
### Changed
|
|
81
|
+
- **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.
|
|
82
|
+
- **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.
|
|
83
|
+
|
|
84
|
+
### Docs
|
|
85
|
+
- `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`.
|
|
86
|
+
|
|
87
|
+
### Tests
|
|
88
|
+
- +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.
|
|
89
|
+
|
|
12
90
|
## [2.49.0] - 2026-07-16
|
|
13
91
|
|
|
14
92
|
### Added
|
package/install/package.json
CHANGED
|
@@ -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
|
|
26
|
+
wroteFiles,
|
|
25
27
|
proposedAgent: gate.hasProposalMarker(text),
|
|
26
|
-
|
|
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
|
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// PreToolUse hook — the armed Codex-delegation guard (v3 : router-obedient +
|
|
5
|
+
// pressure-gated). See lib/codex-delegate-gate.js for the policy.
|
|
6
|
+
//
|
|
7
|
+
// It DENIES a code Write/Edit only when ALL hold : the lane is armed (yanstaller
|
|
8
|
+
// option on AND Codex linked), the ROUTER routes the task to Codex, we are under
|
|
9
|
+
// Claude budget PRESSURE, Codex is available, and no delegation happened this turn
|
|
10
|
+
// — and the human has not opted out. Otherwise it allows. Never traps a turn on an
|
|
11
|
+
// internal error (fails open), always exits 0.
|
|
12
|
+
//
|
|
13
|
+
// The two computed signals (router decision, budget pressure) are only evaluated
|
|
14
|
+
// in the would-deny branch, so the common allow paths pay nothing. Both fail open
|
|
15
|
+
// (an unresolvable signal -> allow, never a wrong block).
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const { spawnSync } = require('child_process');
|
|
20
|
+
const { pathToFileURL } = require('url');
|
|
21
|
+
const gate = require('./lib/codex-delegate-gate');
|
|
22
|
+
const { loadConfig, codexLinked, toggledOff } = require('./codex-autodelegate');
|
|
23
|
+
const { estimateClaudeUsage } = require('./lib/usage-estimator');
|
|
24
|
+
const { extractRecentMessages, contentToText } = require('./lib/transcript-read');
|
|
25
|
+
|
|
26
|
+
const ROOT = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
27
|
+
const ROUTER_PATH = path.resolve(__dirname, '../../_byan/mcp/byan-mcp-server/lib/dispatch-router.js');
|
|
28
|
+
const DEFAULT_PRESSURE_THRESHOLD = 75; // % of the Claude budget window
|
|
29
|
+
|
|
30
|
+
function readStdin() {
|
|
31
|
+
return new Promise((resolve) => {
|
|
32
|
+
let data = '';
|
|
33
|
+
process.stdin.on('data', (c) => (data += c));
|
|
34
|
+
process.stdin.on('end', () => resolve(data));
|
|
35
|
+
process.stdin.on('error', () => resolve(''));
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const allow = () => ({ hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'allow' } });
|
|
40
|
+
const deny = (reason) => ({ hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: reason } });
|
|
41
|
+
|
|
42
|
+
// Codex availability probe : `codex --version` exits 0. Absent/broken -> false ->
|
|
43
|
+
// the guard must NOT deny (staying on Claude is the legit fallback).
|
|
44
|
+
function probeCodexAvailable(runner = spawnSync) {
|
|
45
|
+
try {
|
|
46
|
+
const res = runner('codex', ['--version'], { timeout: 8000, encoding: 'utf8' });
|
|
47
|
+
return res && res.status === 0;
|
|
48
|
+
} catch (_e) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// routerSaysCodex(text) : does dispatch-router route this task's text to Codex?
|
|
54
|
+
// The router keyword-matches on the text (execution/shell/deploy/script...). ESM
|
|
55
|
+
// module dynamically imported from this CJS hook. Fails open (false = don't block)
|
|
56
|
+
// if the router cannot be loaded.
|
|
57
|
+
async function defaultRouteProbe(text) {
|
|
58
|
+
try {
|
|
59
|
+
const mod = await import(pathToFileURL(ROUTER_PATH).href);
|
|
60
|
+
return mod.routeRuntime(String(text || '')) === 'codex';
|
|
61
|
+
} catch (_e) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// underPressure : Claude budget usage >= threshold. Needs a budget in the config
|
|
67
|
+
// (no budget -> pct null -> not under pressure -> no delegation, documented).
|
|
68
|
+
function computeUnderPressure(config) {
|
|
69
|
+
try {
|
|
70
|
+
const budget = config && config.budget ? config.budget : null;
|
|
71
|
+
if (!budget) return false;
|
|
72
|
+
const threshold = typeof config.threshold === 'number' ? config.threshold : DEFAULT_PRESSURE_THRESHOLD;
|
|
73
|
+
const usage = estimateClaudeUsage({ budget });
|
|
74
|
+
return typeof usage.pct === 'number' && usage.pct >= threshold;
|
|
75
|
+
} catch (_e) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function lastUserText(payload) {
|
|
81
|
+
const msgs = extractRecentMessages(payload, 12);
|
|
82
|
+
if (!Array.isArray(msgs)) return '';
|
|
83
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
84
|
+
if (msgs[i] && msgs[i].role === 'user') return contentToText(msgs[i].content);
|
|
85
|
+
}
|
|
86
|
+
return '';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// runGuard — async (the router import is async). opts inject the probes for tests.
|
|
90
|
+
async function runGuard(payload, {
|
|
91
|
+
root = ROOT,
|
|
92
|
+
codexProbe = probeCodexAvailable,
|
|
93
|
+
routeProbe = defaultRouteProbe,
|
|
94
|
+
pressureProbe = computeUnderPressure,
|
|
95
|
+
} = {}) {
|
|
96
|
+
const toolName = payload.tool_name || payload.toolName || '';
|
|
97
|
+
if (toolName !== 'Write' && toolName !== 'Edit') return allow();
|
|
98
|
+
|
|
99
|
+
const config = loadConfig(root);
|
|
100
|
+
const armed = config.enabled === true && codexLinked();
|
|
101
|
+
if (!armed) return allow(); // fast path, no transcript/probe work when off
|
|
102
|
+
|
|
103
|
+
const escaped = toggledOff(root);
|
|
104
|
+
const input = payload.tool_input || payload.toolInput || {};
|
|
105
|
+
const filePath = input.file_path || input.filePath || '';
|
|
106
|
+
const isCode = gate.targetIsCode(filePath);
|
|
107
|
+
const humanOptOut = gate.humanOptOutFromText(lastUserText(payload));
|
|
108
|
+
const delegationSeen = gate.delegationSeenThisTurn(extractRecentMessages(payload, 12) || []);
|
|
109
|
+
|
|
110
|
+
// Only compute the two expensive signals when a deny is otherwise possible.
|
|
111
|
+
const maybeDeny = !escaped && !humanOptOut && isCode && !delegationSeen;
|
|
112
|
+
const routerSaysCodex = maybeDeny ? await routeProbe(lastUserText(payload)) : false;
|
|
113
|
+
const underPressure = (maybeDeny && routerSaysCodex) ? pressureProbe(config) : false;
|
|
114
|
+
const codexAvailable = (maybeDeny && routerSaysCodex && underPressure) ? codexProbe() : true;
|
|
115
|
+
|
|
116
|
+
const decision = gate.decideDelegateGate({
|
|
117
|
+
armed, routerSaysCodex, targetIsCode: isCode, underPressure,
|
|
118
|
+
delegationSeen, escaped, humanOptOut, codexAvailable,
|
|
119
|
+
});
|
|
120
|
+
return decision.decision === 'deny' ? deny(decision.reason) : allow();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (require.main === module) {
|
|
124
|
+
(async () => {
|
|
125
|
+
let out;
|
|
126
|
+
try {
|
|
127
|
+
const payload = JSON.parse((await readStdin()) || '{}');
|
|
128
|
+
out = await runGuard(payload);
|
|
129
|
+
} catch (_e) {
|
|
130
|
+
out = allow();
|
|
131
|
+
}
|
|
132
|
+
process.stdout.write(JSON.stringify(out));
|
|
133
|
+
process.exit(0);
|
|
134
|
+
})();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
module.exports = { runGuard, probeCodexAvailable, defaultRouteProbe, computeUnderPressure, 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.
|
|
51
|
-
// already in play (DISPATCH is part of it), so
|
|
52
|
-
|
|
53
|
-
|
|
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
|
};
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
|
|
23
23
|
const { perfFavors } = require('./perf-routing');
|
|
24
24
|
|
|
25
|
-
const DEFAULT_THRESHOLD =
|
|
25
|
+
const DEFAULT_THRESHOLD = 75;
|
|
26
26
|
const DEFAULT_INVOCATION = 'codex:codex-rescue --model gpt-5.4';
|
|
27
27
|
const RED_LINE = 'delegable work only (code / mechanical) — judgment, analysis, soul and verification stay on Claude';
|
|
28
28
|
|
|
@@ -71,16 +71,12 @@ function decideAutodelegation({ requestText = '', usage = null, config = {} } =
|
|
|
71
71
|
};
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
redLine: RED_LINE,
|
|
81
|
-
invocation,
|
|
82
|
-
};
|
|
83
|
-
}
|
|
74
|
+
// v3 : NATURE alone is no longer an auto-delegate trigger. The user's decision
|
|
75
|
+
// is PRESSURE-only — delegating to the subscription Codex (which trails Claude
|
|
76
|
+
// on quality) is worth it to spare the Claude budget, not per coding task. So a
|
|
77
|
+
// delegable task off-pressure does NOT propose Codex. `delegable` is kept only as
|
|
78
|
+
// a signal the perf-routing opt-in below can still use.
|
|
79
|
+
void delegable;
|
|
84
80
|
|
|
85
81
|
// PERF (opt-in): the forces table (user-populated) may favor Codex for this
|
|
86
82
|
// kind of task even at low pressure. Off by default; always heuristic.
|
|
@@ -141,8 +137,10 @@ function renderNudge(decision) {
|
|
|
141
137
|
`[BYAN auto-delegate]${gauge}: ${scope} via \`${invocation}\` `
|
|
142
138
|
+ '(runs on the ChatGPT subscription, no API credit).',
|
|
143
139
|
`Red line: ${decision.redLine}. The lane is ARMED (option on AND Codex linked), so this is a `
|
|
144
|
-
+ 'DIRECTIVE, not a
|
|
145
|
-
+
|
|
140
|
+
+ 'DIRECTIVE, not a suggestion: delegate the delegable part. The ONLY valid reasons to stay on '
|
|
141
|
+
+ 'Claude are (1) Codex unavailable, or (2) the user asked to stay on Claude. "Small script / '
|
|
142
|
+
+ 'latency / I verify anyway" is NOT a valid reason — the codex-delegate-guard denies a delegable '
|
|
143
|
+
+ "code write that skips Codex. Verify Codex's output before commit.",
|
|
146
144
|
].join(' ');
|
|
147
145
|
}
|
|
148
146
|
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Core of the armed Codex-delegation guard (v3 : router-obedient + pressure-gated).
|
|
4
|
+
//
|
|
5
|
+
// History : v1 blocked every code Write when the lane was armed (blunt). v2
|
|
6
|
+
// (option B) closed BYAN's self-dodge (no content marker, no resubmit auto-pass).
|
|
7
|
+
// v3 makes the block match the actual delegation policy the user chose :
|
|
8
|
+
// - it OBEYS the router : it only bites when dispatch-router routes the task to
|
|
9
|
+
// Codex (execution / shell / deploy). A refactor / architecture / judgment
|
|
10
|
+
// task routes to Claude, so the block stays silent.
|
|
11
|
+
// - it is PRESSURE-gated : delegation to Codex is worth it to spare the Claude
|
|
12
|
+
// budget, not for quality (the subscription Codex trails Claude). So the block
|
|
13
|
+
// only bites when the Claude usage is under budget PRESSURE (>= threshold).
|
|
14
|
+
// Off-pressure, code runs on Claude with no nag.
|
|
15
|
+
//
|
|
16
|
+
// Still true from v2 : the only per-turn escape is the HUMAN's (escape file or an
|
|
17
|
+
// opt-out phrase in the request) — BYAN cannot self-grant a bypass. Codex genuinely
|
|
18
|
+
// unavailable is a legit fallback. A real delegation already done this turn passes
|
|
19
|
+
// (Claude applies the diff).
|
|
20
|
+
//
|
|
21
|
+
// Pure (no I/O) : the hook shell computes the signals (armed, routerSaysCodex,
|
|
22
|
+
// targetIsCode, underPressure, delegationSeen, escaped, humanOptOut, codexAvailable)
|
|
23
|
+
// and feeds them here.
|
|
24
|
+
|
|
25
|
+
const CODE_EXT = new Set([
|
|
26
|
+
'.sh', '.bash', '.zsh', '.js', '.mjs', '.cjs', '.jsx', '.ts', '.tsx',
|
|
27
|
+
'.py', '.rb', '.go', '.rs', '.java', '.kt', '.c', '.h', '.cpp', '.hpp',
|
|
28
|
+
'.cc', '.php', '.pl', '.lua', '.sql', '.swift', '.scala', '.dart',
|
|
29
|
+
]);
|
|
30
|
+
const CODE_BASENAMES = new Set(['Dockerfile', 'Makefile', 'makefile', 'Rakefile']);
|
|
31
|
+
|
|
32
|
+
function extOf(filePath) {
|
|
33
|
+
const s = String(filePath || '');
|
|
34
|
+
const slash = Math.max(s.lastIndexOf('/'), s.lastIndexOf('\\'));
|
|
35
|
+
const base = slash >= 0 ? s.slice(slash + 1) : s;
|
|
36
|
+
const dot = base.lastIndexOf('.');
|
|
37
|
+
return dot > 0 ? base.slice(dot).toLowerCase() : '';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function baseOf(filePath) {
|
|
41
|
+
const s = String(filePath || '');
|
|
42
|
+
const slash = Math.max(s.lastIndexOf('/'), s.lastIndexOf('\\'));
|
|
43
|
+
return slash >= 0 ? s.slice(slash + 1) : s;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// targetIsCode(filePath) — the write is delegable EXECUTABLE code (a doc/config is
|
|
47
|
+
// not). A CI workflow path also counts.
|
|
48
|
+
function targetIsCode(filePath) {
|
|
49
|
+
if (!filePath) return false;
|
|
50
|
+
const norm = String(filePath).split('\\').join('/');
|
|
51
|
+
if (/(^|\/)\.github\/workflows\//.test(norm) && /\.(ya?ml)$/i.test(norm)) return true;
|
|
52
|
+
if (CODE_BASENAMES.has(baseOf(norm))) return true;
|
|
53
|
+
return CODE_EXT.has(extOf(norm));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// The HUMAN opt-out : the only per-turn way to keep a delegable write on Claude is
|
|
57
|
+
// the human saying so in the request. Matched on the user's OWN text.
|
|
58
|
+
const HUMAN_OPTOUT_RE = /\b(reste sur claude|sans codex|pas de codex|no codex|pas de delegation|don'?t delegate|skip codex)\b/i;
|
|
59
|
+
|
|
60
|
+
function humanOptOutFromText(text) {
|
|
61
|
+
return HUMAN_OPTOUT_RE.test(String(text == null ? '' : text));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// delegationSeenThisTurn(messages) — did a real Codex delegation happen this turn?
|
|
65
|
+
// (Bash `codex exec`, a codex:* Task/Agent, or an MCP codex-bridge call.) If so,
|
|
66
|
+
// Claude applying the result is legitimate.
|
|
67
|
+
function delegationSeenThisTurn(messages) {
|
|
68
|
+
if (!Array.isArray(messages)) return false;
|
|
69
|
+
for (const m of messages) {
|
|
70
|
+
if (!m || m.role !== 'assistant' || !Array.isArray(m.content)) continue;
|
|
71
|
+
for (const block of m.content) {
|
|
72
|
+
if (!block || block.type !== 'tool_use') continue;
|
|
73
|
+
const name = String(block.name || '');
|
|
74
|
+
const input = block.input || {};
|
|
75
|
+
if (name === 'Bash' && /\bcodex\s+exec\b/i.test(String(input.command || ''))) return true;
|
|
76
|
+
if ((name === 'Task' || name === 'Agent') && /codex/i.test(String(input.subagent_type || input.subagentType || ''))) return true;
|
|
77
|
+
if (/codex[_-]?bridge|codex[_-]?rescue/i.test(name)) return true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const DENY_REASON = [
|
|
84
|
+
'BYAN souverainete : le routeur (dispatch-router) a route cette tache vers CODEX',
|
|
85
|
+
'(nature execution / shell / script / deploiement), tu es SOUS PRESSION budget',
|
|
86
|
+
'Claude (usage proche de la limite), Codex est disponible, mais tu ecris le code',
|
|
87
|
+
'toi-meme sans avoir delegue ce tour. Delegue a Codex pour epargner le budget',
|
|
88
|
+
'Claude : agent codex:codex-rescue, ou `codex exec` via codex-bridge, puis applique',
|
|
89
|
+
'le diff. Les SEULES raisons valides de rester sur Claude : (1) Codex indisponible,',
|
|
90
|
+
'ou (2) l\'utilisateur a demande de rester sur Claude ("reste sur claude" / "sans',
|
|
91
|
+
'codex", ou le fichier .byan-codex-autodelegate/off). "Script court / latence / je',
|
|
92
|
+
'verifie" n\'est PAS valide. BYAN ne peut pas s\'auto-accorder la sortie.',
|
|
93
|
+
].join(' ');
|
|
94
|
+
|
|
95
|
+
// decideDelegateGate — the pure decision (v3). Order : cheap allows short-circuit
|
|
96
|
+
// before the deny. The block bites ONLY when every condition holds : armed, the
|
|
97
|
+
// ROUTER routes to Codex, the target is code, we are under budget PRESSURE, Codex
|
|
98
|
+
// is available, and no delegation happened this turn. Any miss -> allow. The human
|
|
99
|
+
// always keeps the escape switch / opt-out, so it is never a trap for the user.
|
|
100
|
+
function decideDelegateGate({
|
|
101
|
+
armed = false,
|
|
102
|
+
routerSaysCodex = false,
|
|
103
|
+
targetIsCode: isCode = false,
|
|
104
|
+
underPressure = false,
|
|
105
|
+
delegationSeen = false,
|
|
106
|
+
escaped = false,
|
|
107
|
+
humanOptOut = false,
|
|
108
|
+
codexAvailable = true,
|
|
109
|
+
} = {}) {
|
|
110
|
+
if (!armed) return { decision: 'allow', code: 'lane-not-armed' };
|
|
111
|
+
if (escaped) return { decision: 'allow', code: 'escape-hatch' }; // human switch
|
|
112
|
+
if (humanOptOut) return { decision: 'allow', code: 'human-opt-out' }; // human asked Claude
|
|
113
|
+
if (!routerSaysCodex) return { decision: 'allow', code: 'router-claude' }; // brain routed to Claude (F2)
|
|
114
|
+
if (!isCode) return { decision: 'allow', code: 'not-code' }; // codex-nature task but a doc/config write
|
|
115
|
+
if (!underPressure) return { decision: 'allow', code: 'no-pressure' }; // budget not tight (F1) -> Claude is fine
|
|
116
|
+
if (!codexAvailable) return { decision: 'allow', code: 'codex-unavailable' }; // legit fallback
|
|
117
|
+
if (delegationSeen) return { decision: 'allow', code: 'delegation-seen' }; // Codex already run
|
|
118
|
+
return { decision: 'deny', code: 'delegate-first', reason: DENY_REASON };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
module.exports = {
|
|
122
|
+
CODE_EXT,
|
|
123
|
+
CODE_BASENAMES,
|
|
124
|
+
targetIsCode,
|
|
125
|
+
HUMAN_OPTOUT_RE,
|
|
126
|
+
humanOptOutFromText,
|
|
127
|
+
delegationSeenThisTurn,
|
|
128
|
+
decideDelegateGate,
|
|
129
|
+
DENY_REASON,
|
|
130
|
+
};
|