create-byan-agent 2.39.0 → 2.41.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 +102 -1
- package/install/bin/create-byan-agent-v2.js +40 -0
- package/install/lib/codex-autodelegate-setup.js +100 -0
- package/install/templates/.claude/hooks/codex-autodelegate.js +74 -0
- package/install/templates/.claude/hooks/lib/autodelegate-decision.js +152 -0
- package/install/templates/.claude/hooks/lib/perf-routing.js +47 -0
- package/install/templates/.claude/hooks/lib/usage-estimator.js +262 -0
- package/install/templates/.claude/hooks/tier-script-guard.js +185 -0
- package/install/templates/.claude/rules/native-workflows.md +33 -7
- package/install/templates/.claude/settings.json +13 -0
- package/install/templates/.claude/skills/byan-byan/SKILL.md +7 -2
- package/install/templates/.claude/skills/byan-hermes-dispatch/SKILL.md +3 -1
- package/install/templates/.claude/workflows/sprint-planning.js +2 -2
- package/install/templates/.claude/workflows/testarch-trace.js +3 -2
- package/install/templates/_byan/mcp/byan-mcp-server/bin/byan-tier-script.js +52 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/dispatch.js +22 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/native-tiers.js +26 -9
- package/install/templates/_byan/mcp/byan-mcp-server/lib/tier-script.js +104 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/workflows-lint.js +92 -6
- package/install/templates/_byan/mcp/byan-mcp-server/server.js +22 -7
- package/install/templates/_byan/mcp/byan-mcp-server/skill-bundles-manifest.json +2 -2
- package/install/templates/dist/skill-bundles/byan-byan.zip +0 -0
- package/install/templates/dist/skill-bundles/byan-hermes-dispatch.zip +0 -0
- package/install/templates/docs/native-workflows-contract.md +55 -12
- package/package.json +1 -1
- package/src/loadbalancer/capability-matrix.js +14 -0
- package/src/loadbalancer/degradation-ladder.js +121 -0
- package/src/loadbalancer/loadbalancer.default.yaml +23 -1
- package/src/loadbalancer/mcp-server.js +200 -18
- package/src/loadbalancer/providers/codex-provider.js +260 -0
- package/src/loadbalancer/providers/factory.js +36 -0
- package/src/loadbalancer/subscription-window.js +142 -0
- package/src/loadbalancer/switch-tolerance.js +63 -0
- package/src/loadbalancer/tools/index.js +17 -2
|
@@ -73,3 +73,25 @@ export function dispatch({ task, complexity, parallelizable, nature } = {}) {
|
|
|
73
73
|
reasoning: `${strategyReason}. ${tierReason}.`,
|
|
74
74
|
};
|
|
75
75
|
}
|
|
76
|
+
|
|
77
|
+
// Batch tiering — the authoring aid for workflow scripts. The author passes the
|
|
78
|
+
// planned leaves BEFORE writing the script and gets the opts.model value per
|
|
79
|
+
// leaf from the same source of truth (native-tiers). No strategy axis here:
|
|
80
|
+
// the leaves all run inside one Workflow invocation, so WHERE they run is the
|
|
81
|
+
// script's concern, only WHICH model each deserves is answered. An explicit
|
|
82
|
+
// valid nature wins; otherwise the label classifies; a miss stays protected
|
|
83
|
+
// (implementation -> deep -> null), same conservative path as dispatch().
|
|
84
|
+
export function dispatchBatch(leaves) {
|
|
85
|
+
if (!Array.isArray(leaves)) return [];
|
|
86
|
+
return leaves.map((leaf) => {
|
|
87
|
+
const { label, nature } = leaf && typeof leaf === 'object' ? leaf : {};
|
|
88
|
+
const leafType = VALID_NATURES.has(nature) ? nature : classifyLeaf({ label: label || '' });
|
|
89
|
+
const tier = tierFor(leafType);
|
|
90
|
+
return {
|
|
91
|
+
label: label || '',
|
|
92
|
+
nature: leafType,
|
|
93
|
+
tier,
|
|
94
|
+
model: TIER_MODEL[tier],
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
}
|
|
@@ -28,7 +28,8 @@ export const TIERS = Object.freeze({ CHEAP: 'cheap', BALANCED: 'balanced', DEEP:
|
|
|
28
28
|
// session runs (Opus by default, but Sonnet if the user chose Sonnet). We never
|
|
29
29
|
// PIN UP — pinning a leaf to a fixed high tier would override the user's session
|
|
30
30
|
// choice and could silently DOWNGRADE a Sonnet/Opus session's heavy leaf. Only
|
|
31
|
-
// cheap/balanced carry a value
|
|
31
|
+
// cheap/balanced carry a value: exploration leaves get cheap, explicit mech-
|
|
32
|
+
// leaves get balanced, nothing else ever gets one.
|
|
32
33
|
//
|
|
33
34
|
// Values are the harness model-selection aliases (same set as the Agent tool:
|
|
34
35
|
// 'haiku' | 'sonnet' | 'opus'). They are version-independent. If a future
|
|
@@ -36,15 +37,26 @@ export const TIERS = Object.freeze({ CHEAP: 'cheap', BALANCED: 'balanced', DEEP:
|
|
|
36
37
|
// flags every script literal that drifts from it, so the fan-out stays bounded.
|
|
37
38
|
export const TIER_MODEL = Object.freeze({ cheap: 'haiku', balanced: 'sonnet', deep: null });
|
|
38
39
|
|
|
39
|
-
// Leaf task-type taxonomy. EXPLORATION
|
|
40
|
-
// other three are protected (never downgraded).
|
|
40
|
+
// Leaf task-type taxonomy. EXPLORATION and MECHANICAL are the two
|
|
41
|
+
// downgrade-safe classes; the other three are protected (never downgraded).
|
|
42
|
+
// MECHANICAL is verification whose outcome is binary and judgment-free (JSON
|
|
43
|
+
// parses, schema matches, lint passes, a test suite exits 0) — it earns the
|
|
44
|
+
// balanced tier. Semantic/adversarial verification stays VERIFICATION (deep).
|
|
41
45
|
export const LEAF_TYPES = Object.freeze({
|
|
42
46
|
EXPLORATION: 'exploration',
|
|
47
|
+
MECHANICAL: 'mechanical',
|
|
43
48
|
IMPLEMENTATION: 'implementation',
|
|
44
49
|
VERIFICATION: 'verification',
|
|
45
50
|
ANALYSIS: 'analysis',
|
|
46
51
|
});
|
|
47
52
|
|
|
53
|
+
// MECHANICAL is opt-in ONLY, through this label prefix ('mech-validate-json').
|
|
54
|
+
// No keyword fuzziness: 'validate-json' without the prefix stays VERIFICATION
|
|
55
|
+
// (protected). The prefix is an explicit authoring act — the author asserts
|
|
56
|
+
// "this check is binary and judgment-free", and the linter can then hold the
|
|
57
|
+
// script to it (a mech- leaf must carry model: 'sonnet', nothing else).
|
|
58
|
+
export const MECHANICAL_PREFIX = 'mech-';
|
|
59
|
+
|
|
48
60
|
// Label keyword sets, matched as substrings on the leaf LABEL (not the prompt —
|
|
49
61
|
// see classifyLeaf). Protected sets are checked first so any protected signal
|
|
50
62
|
// beats an exploration signal (conservative: when in doubt, do not downgrade).
|
|
@@ -73,6 +85,9 @@ function matchesAny(text, keywords) {
|
|
|
73
85
|
export function classifyLeaf(leaf) {
|
|
74
86
|
const label = String((leaf && leaf.label) || '').toLowerCase();
|
|
75
87
|
if (!label) return LEAF_TYPES.IMPLEMENTATION;
|
|
88
|
+
// Explicit opt-in beats every keyword class: 'mech-validate-json' contains a
|
|
89
|
+
// verification keyword, but the author's declared intent is the signal.
|
|
90
|
+
if (label.startsWith(MECHANICAL_PREFIX)) return LEAF_TYPES.MECHANICAL;
|
|
76
91
|
if (matchesAny(label, VERIFICATION_KEYWORDS)) return LEAF_TYPES.VERIFICATION;
|
|
77
92
|
if (matchesAny(label, ANALYSIS_KEYWORDS)) return LEAF_TYPES.ANALYSIS;
|
|
78
93
|
if (matchesAny(label, IMPLEMENTATION_KEYWORDS)) return LEAF_TYPES.IMPLEMENTATION;
|
|
@@ -80,13 +95,15 @@ export function classifyLeaf(leaf) {
|
|
|
80
95
|
return LEAF_TYPES.IMPLEMENTATION;
|
|
81
96
|
}
|
|
82
97
|
|
|
83
|
-
// tierFor(taskType) -> a TIERS value. Conservative auto-routing:
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
98
|
+
// tierFor(taskType) -> a TIERS value. Conservative auto-routing: EXPLORATION
|
|
99
|
+
// downgrades to cheap, MECHANICAL to balanced, every other type stays deep.
|
|
100
|
+
// BALANCED is reachable ONLY through the explicit mech- label opt-in (see
|
|
101
|
+
// MECHANICAL_PREFIX) — never through keyword classification — so automation
|
|
102
|
+
// still cannot land it on judgment-bearing work.
|
|
88
103
|
export function tierFor(taskType) {
|
|
89
|
-
|
|
104
|
+
if (taskType === LEAF_TYPES.EXPLORATION) return TIERS.CHEAP;
|
|
105
|
+
if (taskType === LEAF_TYPES.MECHANICAL) return TIERS.BALANCED;
|
|
106
|
+
return TIERS.DEEP;
|
|
90
107
|
}
|
|
91
108
|
|
|
92
109
|
// modelForLeaf({ label }) -> the opts.model value to write (a string) or null
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Per-script tiering report + gate decision (the engine under the tier hook
|
|
2
|
+
// and the byan-tier-script bin).
|
|
3
|
+
//
|
|
4
|
+
// The Workflow tool runs every agent() leaf on the session model unless the
|
|
5
|
+
// script pins opts.model, and the sandbox forbids importing native-tiers at
|
|
6
|
+
// runtime — so the ONLY moment the tiering doctrine can reach an AD-HOC script
|
|
7
|
+
// is when the script text crosses the Workflow tool boundary. This module
|
|
8
|
+
// analyzes that text: one verdict per statically-labelled leaf, against the
|
|
9
|
+
// same source of truth the repo linter enforces (native-tiers.js).
|
|
10
|
+
//
|
|
11
|
+
// The gate is deny-ONCE and never rewrites the script (STRICT-2 No Downgrade:
|
|
12
|
+
// a wrong auto-stamp would be the exact regression the doctrine forbids). The
|
|
13
|
+
// author fixes the listed leaves, or asserts the deep choices are deliberate
|
|
14
|
+
// with the acknowledgment marker. An identical resubmission after a deny
|
|
15
|
+
// passes — the gate forces a decision, it does not trap the turn.
|
|
16
|
+
|
|
17
|
+
import crypto from 'node:crypto';
|
|
18
|
+
import { classifyLeaf, tierFor, TIER_MODEL, TIERS, LEAF_TYPES } from './native-tiers.js';
|
|
19
|
+
import { stripComments, extractLabelledLeaves } from './workflows-lint.js';
|
|
20
|
+
|
|
21
|
+
// Acknowledgment marker, raw-text (comment form survives comment-stripping
|
|
22
|
+
// concerns by design — mirrors the BYAN-BENCH marker family). Writing it is an
|
|
23
|
+
// explicit authoring act: "I reviewed the tiering; the deep leaves are
|
|
24
|
+
// deliberate."
|
|
25
|
+
export const ACK_RE = /BYAN-TIER:\s*reviewed\b/;
|
|
26
|
+
|
|
27
|
+
function verdictFor(cls, model) {
|
|
28
|
+
const expected = TIER_MODEL[tierFor(cls)]; // 'haiku' | 'sonnet' | null (inherit)
|
|
29
|
+
if (expected === null) {
|
|
30
|
+
// Protected class: any pinned model is a downgrade or a pin-up — both wrong.
|
|
31
|
+
return model === null ? 'ok' : 'violation';
|
|
32
|
+
}
|
|
33
|
+
if (model === null) return 'missing-tier';
|
|
34
|
+
if (model === expected) return 'ok';
|
|
35
|
+
// Exploration may ride ABOVE its floor (sonnet on a cheap leaf wastes a
|
|
36
|
+
// little, breaks nothing). Anything else — haiku on mech-, opus anywhere —
|
|
37
|
+
// is below a declared tier or an unknown pin.
|
|
38
|
+
if (cls === LEAF_TYPES.EXPLORATION && model === TIER_MODEL[TIERS.BALANCED]) return 'ok';
|
|
39
|
+
return 'violation';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// analyzeScript(src) -> { acknowledged, agentCalls, leaves, gaps, violations }
|
|
43
|
+
// leaves: [{ label, model, class, expectedModel, verdict }] for every
|
|
44
|
+
// statically-labelled opts object. Unlabelled agent() calls classify to the
|
|
45
|
+
// deep default and cannot gap, so they are only counted (agentCalls).
|
|
46
|
+
export function analyzeScript(src) {
|
|
47
|
+
const leaves = extractLabelledLeaves(src).map(({ label, model }) => {
|
|
48
|
+
const cls = classifyLeaf({ label });
|
|
49
|
+
return {
|
|
50
|
+
label,
|
|
51
|
+
model,
|
|
52
|
+
class: cls,
|
|
53
|
+
expectedModel: TIER_MODEL[tierFor(cls)],
|
|
54
|
+
verdict: verdictFor(cls, model),
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
return {
|
|
58
|
+
acknowledged: ACK_RE.test(String(src)),
|
|
59
|
+
agentCalls: (stripComments(src).match(/\bagent\s*\(/g) || []).length,
|
|
60
|
+
leaves,
|
|
61
|
+
gaps: leaves.filter((l) => l.verdict === 'missing-tier'),
|
|
62
|
+
violations: leaves.filter((l) => l.verdict === 'violation'),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// One line per offending leaf, then the two exits (fix or acknowledge). The
|
|
67
|
+
// reason is the whole teaching surface of the gate — it must carry the exact
|
|
68
|
+
// label and the exact value to write.
|
|
69
|
+
export function formatGateReason(analysis) {
|
|
70
|
+
const lines = ['BYAN tier gate: this workflow script has undecided or invalid model tiers.'];
|
|
71
|
+
for (const g of analysis.gaps) {
|
|
72
|
+
lines.push(`- leaf '${g.label}' (${g.class}) has no model: add model: '${g.expectedModel}'`);
|
|
73
|
+
}
|
|
74
|
+
for (const v of analysis.violations) {
|
|
75
|
+
const fix = v.expectedModel === null ? 'omit model: (deep leaves inherit the session model)' : `use model: '${v.expectedModel}'`;
|
|
76
|
+
lines.push(`- leaf '${v.label}' (${v.class}) carries model: '${v.model}': ${fix}`);
|
|
77
|
+
}
|
|
78
|
+
lines.push(
|
|
79
|
+
"Fix the listed leaves, or add the comment '// BYAN-TIER: reviewed' to assert the current tiers are deliberate. This gate denies once: an identical resubmission passes."
|
|
80
|
+
);
|
|
81
|
+
return lines.join('\n');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Pure gate decision (the hook is only the I/O shell around this).
|
|
85
|
+
// Precedence: escape hatch > acknowledgment > clean > deny-once memory > deny.
|
|
86
|
+
export function decideTierGate({ analysis, escaped = false, scriptHash = null, priorDenyHash = null } = {}) {
|
|
87
|
+
if (escaped) return { decision: 'allow', code: 'escape-hatch' };
|
|
88
|
+
if (analysis.acknowledged) return { decision: 'allow', code: 'acknowledged' };
|
|
89
|
+
if (analysis.gaps.length === 0 && analysis.violations.length === 0) {
|
|
90
|
+
return { decision: 'allow', code: 'clean' };
|
|
91
|
+
}
|
|
92
|
+
if (scriptHash && priorDenyHash && scriptHash === priorDenyHash) {
|
|
93
|
+
return { decision: 'allow', code: 'unchanged-after-deny' };
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
decision: 'deny',
|
|
97
|
+
code: analysis.violations.length ? 'violations' : 'gaps',
|
|
98
|
+
reason: formatGateReason(analysis),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function hashScript(src) {
|
|
103
|
+
return crypto.createHash('sha1').update(String(src)).digest('hex');
|
|
104
|
+
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import fs from 'node:fs';
|
|
14
14
|
import path from 'node:path';
|
|
15
|
-
import { isKnownTierModel, isDowngradeModel, classifyLeaf, LEAF_TYPES } from './native-tiers.js';
|
|
15
|
+
import { isKnownTierModel, isDowngradeModel, classifyLeaf, LEAF_TYPES, TIER_MODEL } from './native-tiers.js';
|
|
16
16
|
|
|
17
17
|
// Strip /* block */ and // line comments. Preserve "://" inside strings (URLs)
|
|
18
18
|
// by only treating // as a comment when not preceded by a colon.
|
|
@@ -104,6 +104,30 @@ export function metaLiteralViolations(src) {
|
|
|
104
104
|
const MODEL_RE = /\bmodel:\s*(['"`])([^'"`]*)\1/g;
|
|
105
105
|
const LABEL_RE = /\blabel:\s*(['"`])([^'"`]*)\1/g;
|
|
106
106
|
|
|
107
|
+
// Remove the `export const meta = { ... }` literal before scanning model
|
|
108
|
+
// tokens. The harness meta spec allows `model` on a phases entry (a per-phase
|
|
109
|
+
// display/override declaration) — it carries no label by design and must not
|
|
110
|
+
// read as a leaf downgrade. Balanced-brace walk; no-op when unbalanced.
|
|
111
|
+
// String-unaware by choice: a brace inside a meta string unbalances the walk,
|
|
112
|
+
// which then no-ops and the meta model token stays visible to the scan — the
|
|
113
|
+
// failure mode OVER-reports (fails closed), never hides a real violation.
|
|
114
|
+
// Exported: the routing integration test scans sources with its own regexes
|
|
115
|
+
// (an independent double-check) but must share THIS meta-handling.
|
|
116
|
+
export function stripMetaLiteral(code) {
|
|
117
|
+
const start = code.search(/export const meta\s*=\s*\{/);
|
|
118
|
+
if (start === -1) return code;
|
|
119
|
+
const open = code.indexOf('{', start);
|
|
120
|
+
let depth = 0;
|
|
121
|
+
for (let i = open; i < code.length; i++) {
|
|
122
|
+
if (code[i] === '{') depth += 1;
|
|
123
|
+
else if (code[i] === '}') {
|
|
124
|
+
depth -= 1;
|
|
125
|
+
if (depth === 0) return code.slice(0, open + 1) + code.slice(i);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return code;
|
|
129
|
+
}
|
|
130
|
+
|
|
107
131
|
function nearestLabelBefore(code, modelIndex) {
|
|
108
132
|
const before = code.slice(0, modelIndex);
|
|
109
133
|
let last = null;
|
|
@@ -120,7 +144,7 @@ function nearestLabelBefore(code, modelIndex) {
|
|
|
120
144
|
}
|
|
121
145
|
|
|
122
146
|
export function modelRoutingViolations(src) {
|
|
123
|
-
const code = stripComments(src);
|
|
147
|
+
const code = stripMetaLiteral(stripComments(src));
|
|
124
148
|
const out = [];
|
|
125
149
|
let m;
|
|
126
150
|
MODEL_RE.lastIndex = 0;
|
|
@@ -137,14 +161,56 @@ export function modelRoutingViolations(src) {
|
|
|
137
161
|
if (!label) {
|
|
138
162
|
out.push({
|
|
139
163
|
id: 'downgrade-without-label',
|
|
140
|
-
msg: `a model downgrade ('${model}') must sit on a labelled exploration leaf; no label found in this opts object`,
|
|
164
|
+
msg: `a model downgrade ('${model}') must sit on a labelled exploration/mech- leaf; no label found in this opts object`,
|
|
141
165
|
});
|
|
142
166
|
continue;
|
|
143
167
|
}
|
|
144
|
-
if (isDowngradeModel(model)
|
|
168
|
+
if (!isDowngradeModel(model)) continue;
|
|
169
|
+
// Per-class floor: exploration accepts any downgrade tier (haiku or sonnet,
|
|
170
|
+
// both at-or-above its cheap floor); a mech- leaf accepts exactly the
|
|
171
|
+
// balanced tier (haiku would sit BELOW the tier its label declares); every
|
|
172
|
+
// protected class refuses both (STRICT-2 No Downgrade).
|
|
173
|
+
const cls = classifyLeaf({ label });
|
|
174
|
+
if (cls === LEAF_TYPES.EXPLORATION) continue;
|
|
175
|
+
if (cls === LEAF_TYPES.MECHANICAL) {
|
|
176
|
+
if (model !== TIER_MODEL.balanced) {
|
|
177
|
+
out.push({
|
|
178
|
+
id: 'mechanical-below-tier',
|
|
179
|
+
msg: `mech- leaf '${label}' carries '${model}' but the mechanical tier is '${TIER_MODEL.balanced}'; a declared-mechanical check must not drop further`,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
out.push({
|
|
185
|
+
id: 'protected-leaf-downgraded',
|
|
186
|
+
msg: `leaf '${label}' is protected (${cls}) but carries downgrade model '${model}'; only exploration (read/load/parse/detect) and explicit mech- leaves may downgrade (STRICT-2 No Downgrade)`,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
return out;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// MECHANICAL opt-in consistency (HARD rule, part of validateContract).
|
|
193
|
+
//
|
|
194
|
+
// A mech- label is an explicit authoring declaration: "this check is binary and
|
|
195
|
+
// judgment-free, run it on the balanced tier". Declaring it and then omitting
|
|
196
|
+
// opts.model half-applies the opt-in — the leaf silently runs deep, which is
|
|
197
|
+
// exactly the waste the label promised to avoid. Since the prefix exists only
|
|
198
|
+
// as this convention (no legacy labels carry it), enforcing it hard cannot trip
|
|
199
|
+
// legitimate work. Order-independent via sameOptsObjectText, comment-stripped.
|
|
200
|
+
export function mechanicalLabelViolations(src) {
|
|
201
|
+
const code = stripComments(src);
|
|
202
|
+
const out = [];
|
|
203
|
+
let m;
|
|
204
|
+
LABEL_RE.lastIndex = 0;
|
|
205
|
+
while ((m = LABEL_RE.exec(code))) {
|
|
206
|
+
const label = m[2];
|
|
207
|
+
if (classifyLeaf({ label }) !== LEAF_TYPES.MECHANICAL) continue;
|
|
208
|
+
const objText = sameOptsObjectText(code, m.index, m.index + m[0].length);
|
|
209
|
+
MODEL_RE.lastIndex = 0;
|
|
210
|
+
if (!MODEL_RE.exec(objText)) {
|
|
145
211
|
out.push({
|
|
146
|
-
id: '
|
|
147
|
-
msg: `leaf '${label}'
|
|
212
|
+
id: 'mechanical-without-model',
|
|
213
|
+
msg: `mech- leaf '${label}' declares a mechanical check but omits opts.model; add model: '${TIER_MODEL.balanced}' (or drop the mech- prefix if the check bears judgment)`,
|
|
148
214
|
});
|
|
149
215
|
}
|
|
150
216
|
}
|
|
@@ -208,6 +274,25 @@ export function untieredExplorationViolations(src) {
|
|
|
208
274
|
return out;
|
|
209
275
|
}
|
|
210
276
|
|
|
277
|
+
// Shared parsing primitive — every statically-labelled opts object with its
|
|
278
|
+
// order-independent model (or null). This is the ONE place that knows how to
|
|
279
|
+
// read agent() opts out of a script source; tier-script.js (the per-leaf
|
|
280
|
+
// report + hook gate) consumes it instead of re-owning the regexes. Fresh
|
|
281
|
+
// regex instances per call: no shared lastIndex across modules.
|
|
282
|
+
export function extractLabelledLeaves(src) {
|
|
283
|
+
const code = stripComments(src);
|
|
284
|
+
const out = [];
|
|
285
|
+
const labelRe = new RegExp(LABEL_RE.source, 'g');
|
|
286
|
+
let m;
|
|
287
|
+
while ((m = labelRe.exec(code))) {
|
|
288
|
+
const objText = sameOptsObjectText(code, m.index, m.index + m[0].length);
|
|
289
|
+
const modelRe = new RegExp(MODEL_RE.source, 'g');
|
|
290
|
+
const mm = modelRe.exec(objText);
|
|
291
|
+
out.push({ label: m[2], model: mm ? mm[2] : null });
|
|
292
|
+
}
|
|
293
|
+
return out;
|
|
294
|
+
}
|
|
295
|
+
|
|
211
296
|
// Full native-workflow contract: state-coupling (comment-stripped) + clock/RNG
|
|
212
297
|
// (raw) + meta-literal-first + model-routing anti-downgrade. Returns the
|
|
213
298
|
// combined [{ id, msg }] violations.
|
|
@@ -226,6 +311,7 @@ export function validateContract(src) {
|
|
|
226
311
|
...clockRngViolations(src),
|
|
227
312
|
...metaLiteralViolations(src),
|
|
228
313
|
...modelRoutingViolations(src),
|
|
314
|
+
...mechanicalLabelViolations(src),
|
|
229
315
|
];
|
|
230
316
|
}
|
|
231
317
|
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
CallToolRequestSchema,
|
|
9
9
|
ListToolsRequestSchema,
|
|
10
10
|
} from '@modelcontextprotocol/sdk/types.js';
|
|
11
|
-
import { dispatch } from './lib/dispatch.js';
|
|
11
|
+
import { dispatch, dispatchBatch } from './lib/dispatch.js';
|
|
12
12
|
import { resolveConfig } from './lib/resolve-config.js';
|
|
13
13
|
import { harvest as harvestInsights, renderDigest as renderInsightDigest } from './lib/insight-harvest.js';
|
|
14
14
|
import { appendOutcome } from './lib/outcome-buffer.js';
|
|
@@ -333,11 +333,11 @@ const tools = [
|
|
|
333
333
|
{
|
|
334
334
|
name: 'byan_dispatch',
|
|
335
335
|
description:
|
|
336
|
-
'BYAN Dispatcher: routes a unit of work along two independent axes. STRATEGY (where it runs: main-thread / agent-subagent-worktree / mcp-worker) from the scalar score + parallelizable. TIER (which model) from the task NATURE via native-tiers (the single source of truth):
|
|
336
|
+
'BYAN Dispatcher: routes a unit of work along two independent axes. STRATEGY (where it runs: main-thread / agent-subagent-worktree / mcp-worker) from the scalar score + parallelizable. TIER (which model) from the task NATURE via native-tiers (the single source of truth): exploration downgrades to haiku, explicit mechanical checks to sonnet; implementation/verification/analysis stay deep (inherit the session model); never pins up to opus. Rule-based, no API call. Returns { score, strategy, nature, tier, model, reasoning }. BATCH mode: pass `leaves` (array of { label, nature? }) to tier every agent() leaf of a workflow script BEFORE writing it — returns one { label, nature, tier, model } per leaf, no strategy axis.',
|
|
337
337
|
inputSchema: {
|
|
338
338
|
type: 'object',
|
|
339
339
|
properties: {
|
|
340
|
-
task: { type: 'string', description: 'Short task description.' },
|
|
340
|
+
task: { type: 'string', description: 'Short task description. Required unless `leaves` is passed (batch mode).' },
|
|
341
341
|
complexity: {
|
|
342
342
|
type: 'number',
|
|
343
343
|
description: 'Complexity score 0-100 (optional, will estimate from task length if absent).',
|
|
@@ -348,11 +348,26 @@ const tools = [
|
|
|
348
348
|
},
|
|
349
349
|
nature: {
|
|
350
350
|
type: 'string',
|
|
351
|
-
enum: ['exploration', 'implementation', 'verification', 'analysis'],
|
|
352
|
-
description: 'Optional task nature. A valid value sets the model tier directly; otherwise the nature is classified from the task text.
|
|
351
|
+
enum: ['exploration', 'mechanical', 'implementation', 'verification', 'analysis'],
|
|
352
|
+
description: 'Optional task nature. A valid value sets the model tier directly; otherwise the nature is classified from the task text. Exploration (haiku) and mechanical (sonnet) are the only downgrade-safe natures.',
|
|
353
|
+
},
|
|
354
|
+
leaves: {
|
|
355
|
+
type: 'array',
|
|
356
|
+
description: 'Batch mode: the planned agent() leaves of a workflow script, each { label, nature? }. Returns the opts.model value per leaf; write model: only where it is non-null.',
|
|
357
|
+
items: {
|
|
358
|
+
type: 'object',
|
|
359
|
+
properties: {
|
|
360
|
+
label: { type: 'string', description: 'The leaf label (the curated signal classifyLeaf keys on).' },
|
|
361
|
+
nature: {
|
|
362
|
+
type: 'string',
|
|
363
|
+
enum: ['exploration', 'mechanical', 'implementation', 'verification', 'analysis'],
|
|
364
|
+
description: 'Optional explicit nature; wins over label classification.',
|
|
365
|
+
},
|
|
366
|
+
},
|
|
367
|
+
additionalProperties: false,
|
|
368
|
+
},
|
|
353
369
|
},
|
|
354
370
|
},
|
|
355
|
-
required: ['task'],
|
|
356
371
|
additionalProperties: false,
|
|
357
372
|
},
|
|
358
373
|
},
|
|
@@ -1561,7 +1576,7 @@ export function createByanServer({ token, remoteOnly = false } = {}) {
|
|
|
1561
1576
|
}
|
|
1562
1577
|
|
|
1563
1578
|
if (name === 'byan_dispatch') {
|
|
1564
|
-
const result = dispatch(args);
|
|
1579
|
+
const result = Array.isArray(args.leaves) ? dispatchBatch(args.leaves) : dispatch(args);
|
|
1565
1580
|
return {
|
|
1566
1581
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
1567
1582
|
};
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
"name": "byan-byan",
|
|
95
95
|
"module": "core",
|
|
96
96
|
"tier": "connector-bound",
|
|
97
|
-
"sourceHash": "
|
|
97
|
+
"sourceHash": "a0d73cf82fcf228965de722e7d01c785ae79a6382975e3045b4eeb902633062a"
|
|
98
98
|
},
|
|
99
99
|
"byan-byan-v2": {
|
|
100
100
|
"name": "byan-byan-v2",
|
|
@@ -184,7 +184,7 @@
|
|
|
184
184
|
"name": "byan-hermes-dispatch",
|
|
185
185
|
"module": "core",
|
|
186
186
|
"tier": "connector-bound",
|
|
187
|
-
"sourceHash": "
|
|
187
|
+
"sourceHash": "e890a80916fedcf888170f550fc4d7ce289f6dc15243d00675e72e80c8579fdb"
|
|
188
188
|
},
|
|
189
189
|
"byan-insight": {
|
|
190
190
|
"name": "byan-insight",
|
|
Binary file
|
|
Binary file
|
|
@@ -96,20 +96,29 @@ tier vocabulary, the leaf classifier, and the model map.
|
|
|
96
96
|
| Tier | `opts.model` | Used for |
|
|
97
97
|
|------|--------------|----------|
|
|
98
98
|
| `deep` | **omitted** (inherit the session model) | implement, verify, analysis — the default |
|
|
99
|
-
| `balanced` | `sonnet` |
|
|
99
|
+
| `balanced` | `sonnet` | MECHANICAL verification, opt-in only via the `mech-` label prefix |
|
|
100
100
|
| `cheap` | `haiku` | a pure exploration leaf: read / load / parse / detect |
|
|
101
101
|
|
|
102
|
-
|
|
102
|
+
Three hard rules:
|
|
103
103
|
|
|
104
104
|
- **No pin-up.** `deep` is an omission, not `model: 'opus'`. Omitting lets a
|
|
105
105
|
leaf inherit whatever the session runs — Opus by default, Sonnet if the user
|
|
106
106
|
chose Sonnet. Pinning a fixed high tier would override that and could silently
|
|
107
107
|
downgrade a Sonnet/Opus session's heavy leaf.
|
|
108
|
-
- **Only exploration
|
|
109
|
-
unambiguous read/extract work. `classifyLeaf` keys off the LABEL
|
|
110
|
-
is too noisy — an exploration leaf often says "report what you
|
|
111
|
-
Protected types (implementation / verification / analysis) and any
|
|
112
|
-
label default to `deep`.
|
|
108
|
+
- **Only exploration and mech- downgrade.** A leaf is pinned to `cheap` only
|
|
109
|
+
when it is unambiguous read/extract work. `classifyLeaf` keys off the LABEL
|
|
110
|
+
(the prompt is too noisy — an exploration leaf often says "report what you
|
|
111
|
+
found"). Protected types (implementation / verification / analysis) and any
|
|
112
|
+
unknown label default to `deep`.
|
|
113
|
+
- **`mech-` is a held declaration.** The `balanced` tier is reachable ONLY
|
|
114
|
+
through the explicit `mech-` label prefix (`mech-validate-json`): a binary,
|
|
115
|
+
judgment-free check — JSON parses, schema matches, lint passes — whose FIXING
|
|
116
|
+
(if any) happens in a different leaf. The prefix wins over keyword
|
|
117
|
+
classification (that is the point of the opt-in), and the linter holds the
|
|
118
|
+
script to it: a `mech-` leaf with no model (`mechanical-without-model`) or
|
|
119
|
+
with `haiku` (`mechanical-below-tier`) is a hard violation. Verification that
|
|
120
|
+
reads MEANING (content vs requirements, coverage adequacy, adversarial
|
|
121
|
+
review) is not mechanical and stays deep.
|
|
113
122
|
|
|
114
123
|
The classifier is permissive (it labels by keyword), so it is a FLOOR, not a
|
|
115
124
|
ceiling: the linter forbids downgrading a protected leaf, but it does not force
|
|
@@ -146,13 +155,15 @@ verbatim/gate sink, making the re-read net-negative or marginal.
|
|
|
146
155
|
Enforcement (because the in-session hooks do not fire inside a script):
|
|
147
156
|
|
|
148
157
|
- `workflows-lint.js` -> `modelRoutingViolations` rejects (a) a `model:` value
|
|
149
|
-
that is not a known downgrade tier, (b) a downgrade on a
|
|
150
|
-
(`protected-leaf-downgraded`), (c) a downgrade with no in-object label
|
|
151
|
-
|
|
152
|
-
|
|
158
|
+
that is not a known downgrade tier, (b) a downgrade on a protected leaf
|
|
159
|
+
(`protected-leaf-downgraded`), (c) a downgrade with no in-object label,
|
|
160
|
+
(d) `haiku` on a `mech-` leaf (`mechanical-below-tier`); and
|
|
161
|
+
`mechanicalLabelViolations` rejects a `mech-` leaf with no model at all
|
|
162
|
+
(`mechanical-without-model`). Both are part of `validateContract`, so
|
|
163
|
+
`byan-lint-workflows` and the pre-commit gate enforce them.
|
|
153
164
|
- `test/native-routing-integration.test.js` pins the invariant on the SHIPPED
|
|
154
165
|
scripts: every script passes the contract, and every downgrade sits on an
|
|
155
|
-
exploration leaf.
|
|
166
|
+
exploration or `mech-` leaf.
|
|
156
167
|
- `workflows-lint.js` -> `untieredExplorationViolations` is the SYMMETRIC
|
|
157
168
|
advisory: it surfaces an exploration-labelled leaf that runs deep (a possible
|
|
158
169
|
saving). It is DELIBERATELY OUT of `validateContract` — forcing those leaves to
|
|
@@ -162,6 +173,38 @@ Enforcement (because the in-session hooks do not fire inside a script):
|
|
|
162
173
|
knob (the native `agent()` / Agent API exposes only `model`), so model tier is
|
|
163
174
|
the sole token lever and effort-by-complexity reduces to model-by-complexity.
|
|
164
175
|
|
|
176
|
+
## Ad-hoc scripts — the tier gate at the Workflow chokepoint
|
|
177
|
+
|
|
178
|
+
The repo linter only sees committed `.claude/workflows/*.js`. A script authored
|
|
179
|
+
inline for one run (the "adversarial review" pattern) is invisible to it and
|
|
180
|
+
used to run every leaf on the session model. It crosses exactly one enforceable
|
|
181
|
+
chokepoint before executing — the Workflow tool invocation — and that is where
|
|
182
|
+
the gate now sits:
|
|
183
|
+
|
|
184
|
+
| Piece | File | Role |
|
|
185
|
+
|-------|------|------|
|
|
186
|
+
| Engine | `lib/tier-script.js` | per-leaf report (`analyzeScript`) + pure gate decision (`decideTierGate`) against native-tiers; parsing stays in `workflows-lint.js` (`extractLabelledLeaves`) |
|
|
187
|
+
| Hook | `.claude/hooks/tier-script-guard.js` | PreToolUse (matcher `Workflow`), CJS shell around the engine |
|
|
188
|
+
| CLI | `bin/byan-tier-script.js` | standalone report on any script file; exit 0 clean/acknowledged, 1 gaps, 2 violations |
|
|
189
|
+
| Batch aid | `byan_dispatch { leaves: [...] }` | per-leaf `opts.model` BEFORE the script is written |
|
|
190
|
+
| Ledger | `_byan-output/tier-ledger.jsonl` | every gate decision + per-model histogram — the measurement basis for token gains |
|
|
191
|
+
|
|
192
|
+
Gate behaviour, deliberately narrow:
|
|
193
|
+
|
|
194
|
+
- **Deny once, with the exact fix.** Undecided exploration/`mech-` leaves deny
|
|
195
|
+
the invocation ONE time, listing each leaf and the value to write. An
|
|
196
|
+
identical resubmission passes — the gate forces a decision, it does not trap
|
|
197
|
+
the turn (same one-regen shape as the autobench Stop hook).
|
|
198
|
+
- **It rewrites nothing.** An auto-stamped model on a misclassified leaf would
|
|
199
|
+
be the STRICT-2 No Downgrade regression; the author keeps the pen.
|
|
200
|
+
- **Acknowledgment marker.** `// BYAN-TIER: reviewed` in the script asserts the
|
|
201
|
+
deep choices are deliberate; the gate then passes and logs `acknowledged`.
|
|
202
|
+
- **Registry passes.** A name-only invocation resolves to a committed script the
|
|
203
|
+
pre-commit linter already owns.
|
|
204
|
+
- **Escape hatch.** `touch .byan-tier/off` disables gating; the ledger still
|
|
205
|
+
records `escape-hatch` so misses stay auditable. Deny-once memory lives in the
|
|
206
|
+
gitignored `.byan-tier/` sidecar, keyed by script hash.
|
|
207
|
+
|
|
165
208
|
If a future runtime needs full model ids instead of the `haiku`/`sonnet`
|
|
166
209
|
aliases, `TIER_MODEL` in `native-tiers.js` is the only edit; the linter then
|
|
167
210
|
flags every script literal that drifts from it, so the fan-out stays bounded.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-byan-agent",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.41.0",
|
|
4
4
|
"description": "BYAN v2.8 - Intelligent AI agent creator with ELO trust system + scientific fact-check + Hermes universal dispatcher + native Claude Code integration (hooks, skills, MCP server). Multi-platform (Claude Code, Codex). Merise Agile + TDD + 71 Mantras. ~54% LLM cost savings.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -35,6 +35,20 @@ const DEFAULT_CAPABILITIES = {
|
|
|
35
35
|
tool_use: true,
|
|
36
36
|
image_input: true,
|
|
37
37
|
},
|
|
38
|
+
codex: {
|
|
39
|
+
file_edit: true,
|
|
40
|
+
file_read: true,
|
|
41
|
+
bash: true,
|
|
42
|
+
web_search: false,
|
|
43
|
+
mcp_tools: true,
|
|
44
|
+
subagents: false,
|
|
45
|
+
git: true,
|
|
46
|
+
streaming: false, // codex exec is request/response (JSONL events, not token stream)
|
|
47
|
+
multi_turn: true, // thread id enables continuation
|
|
48
|
+
max_context_tokens: 256000,
|
|
49
|
+
tool_use: true,
|
|
50
|
+
image_input: false,
|
|
51
|
+
},
|
|
38
52
|
byan_api: {
|
|
39
53
|
file_edit: false,
|
|
40
54
|
file_read: false,
|