create-byan-agent 2.60.1 → 2.60.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +10 -0
  3. package/install/bin/byan-communication.cjs +11 -0
  4. package/install/lib/communication-manifest.json +243 -0
  5. package/install/lib/communication-migration.cjs +91 -0
  6. package/install/lib/communication-previous.json +181 -0
  7. package/install/lib/gitignore.js +135 -0
  8. package/install/lib/install-engine.js +17 -1
  9. package/install/package.json +1 -1
  10. package/install/templates/.claude/agents/bmad-byan.md +13 -26
  11. package/install/templates/.claude/agents/bmad-tao.md +47 -0
  12. package/install/templates/.claude/hooks/auto-dispatch-gate.js +81 -0
  13. package/install/templates/.claude/hooks/inject-tao.js +15 -71
  14. package/install/templates/.claude/hooks/inject-voice-anchor.js +21 -154
  15. package/install/templates/.claude/hooks/lib/dispatch-approval.cjs +135 -0
  16. package/install/templates/.claude/hooks/lib/dispatch-plan-format.js +293 -0
  17. package/install/templates/.claude/hooks/lib/plain-language.js +7 -14
  18. package/install/templates/.claude/hooks/lib/rtk-coverage.js +31 -0
  19. package/install/templates/.claude/hooks/lib/tool-log.js +143 -0
  20. package/install/templates/.claude/hooks/lib/voice-conformance.js +9 -32
  21. package/install/templates/.claude/hooks/lib/webfetch-output.js +37 -0
  22. package/install/templates/.claude/hooks/rtk-coverage.js +18 -0
  23. package/install/templates/.claude/hooks/tool-failure-guard.js +16 -9
  24. package/install/templates/.claude/hooks/webfetch-output.js +17 -0
  25. package/install/templates/.claude/rules/native-workflows.md +24 -5
  26. package/install/templates/.claude/rules/plain-language.md +7 -86
  27. package/install/templates/.claude/settings.json +56 -0
  28. package/install/templates/.claude/skills/byan-byan/SKILL.md +42 -83
  29. package/install/templates/.claude/skills/byan-hermes-dispatch/SKILL.md +22 -7
  30. package/install/templates/.claude/workflows/byan-auto-dispatch.js +86 -38
  31. package/install/templates/.codex/skills/byan/SKILL.md +7 -0
  32. package/install/templates/AGENTS.md +106 -0
  33. package/install/templates/_byan/_config/communication-policy.json +50 -0
  34. package/install/templates/_byan/_config/workflow-manifest.csv +2 -0
  35. package/install/templates/_byan/agent/byan/byan-soul.md +14 -2
  36. package/install/templates/_byan/agent/byan/byan-tao.md +35 -274
  37. package/install/templates/_byan/agent/byan/byan.md +6 -2
  38. package/install/templates/_byan/agent/byan/soul.md +419 -0
  39. package/install/templates/_byan/agent/byan/tao.md +60 -0
  40. package/install/templates/_byan/agent/tao/tao.md +26 -27
  41. package/install/templates/_byan/connaissance/mantras-sources.md +295 -0
  42. package/install/templates/_byan/core/activation/soul-activation.md +6 -5
  43. package/install/templates/_byan/core/communication.cjs +76 -0
  44. package/install/templates/_byan/mcp/byan-mcp-server/lib/agent-matcher.js +38 -1
  45. package/install/templates/_byan/mcp/byan-mcp-server/lib/communication.cjs +77 -0
  46. package/install/templates/_byan/mcp/byan-mcp-server/lib/dispatch-router.js +12 -3
  47. package/install/templates/_byan/mcp/byan-mcp-server/lib/dispatch.js +51 -20
  48. package/install/templates/_byan/mcp/byan-mcp-server/lib/fd-ack.js +75 -0
  49. package/install/templates/_byan/mcp/byan-mcp-server/lib/leantime-fd-core.js +12 -1
  50. package/install/templates/_byan/mcp/byan-mcp-server/lib/native-tiers.js +114 -2
  51. package/install/templates/_byan/mcp/byan-mcp-server/lib/routing-ledger.js +356 -0
  52. package/install/templates/_byan/mcp/byan-mcp-server/lib/soul.js +9 -0
  53. package/install/templates/_byan/mcp/byan-mcp-server/server.js +24 -6
  54. package/install/templates/_byan/mcp/byan-mcp-server/skill-bundles-manifest.json +2 -2
  55. package/install/templates/_byan/workflow/simple/byan/data/mantras.yaml +2 -5
  56. package/install/templates/_byan/workflow/simple/byan/templates/tao-template.md +56 -0
  57. package/install/templates/docs/auto-dispatch.md +76 -0
  58. package/install/templates/docs/communication.md +69 -0
  59. package/install/templates/docs/native-workflows-contract.md +33 -11
  60. package/install/templates/docs/tool-output-cost.md +88 -0
  61. package/install/templates/scripts/communication-eval.cjs +192 -0
  62. package/install/templates/scripts/rtk-coverage-report.js +30 -0
  63. package/install/templates/src/byan-v2/data/mantras.json +1058 -0
  64. package/install/templates/tests/communication/README.md +106 -0
  65. package/install/templates/tests/communication/incises.test.cjs +26 -0
  66. package/install/templates/tests/communication/scenarios.json +391 -0
  67. package/package.json +1 -1
  68. package/src/byan-v2/data/mantras.json +26 -6
@@ -1,4 +1,14 @@
1
- import { classifyLeaf, tierFor, TIER_MODEL, LEAF_TYPES } from './native-tiers.js';
1
+ import {
2
+ classifyLeaf,
3
+ tierFor,
4
+ TIER_MODEL,
5
+ LEAF_TYPES,
6
+ modelForTask,
7
+ effortForTask,
8
+ tierOfModel,
9
+ COMPLEXITY_SOURCES,
10
+ PROTECTED_FLOOR_COMPLEXITY,
11
+ } from './native-tiers.js';
2
12
 
3
13
  // byan_dispatch routes a unit of work along TWO independent axes:
4
14
  //
@@ -6,16 +16,22 @@ import { classifyLeaf, tierFor, TIER_MODEL, LEAF_TYPES } from './native-tiers.js
6
16
  // Derived from the scalar score + parallelizable. This is
7
17
  // dispatch's own concern: orchestration.
8
18
  // TIER — WHICH model the work deserves. Delegated to native-tiers (the
9
- // single source of truth), keyed on the task's NATURE, never on its
10
- // size. Only exploration downgrades to a cheap tier; implementation,
11
- // verification, analysis (and anything unmatched) stay deep =
12
- // inherit the session model. We never PIN UP to opus.
19
+ // single source of truth), au croisement de la NATURE et de la
20
+ // COMPLEXITE DECLAREE de la tache.
13
21
  //
14
22
  // Before this split the two axes were fused into one route string
15
23
  // ('mcp-worker-haiku', 'main-thread-opus'), so a short sequential task was
16
24
  // silently downgraded to haiku purely on length, and a long one was pinned up to
17
25
  // opus — exactly the size-driven mis-tiering native-tiers' anti-downgrade doctrine
18
- // forbids. The score still picks the strategy; the model now comes from nature.
26
+ // forbids. The score still picks the strategy; the model comes from the crossing.
27
+ //
28
+ // La nature seule ne suffisait pas non plus : elle rendait le meme verdict pour
29
+ // un hello world et pour un moteur de resolution de dependances, tous deux de
30
+ // l'implementation, donc tous deux sur le modele de session — le plus cher. La
31
+ // complexite tranche desormais entre les deux, mais SEULEMENT quand un appelant
32
+ // l'a declaree. Sans elle il ne reste que la longueur du prompt, qui mesure des
33
+ // caracteres, pas de la difficulte : la faire decider du modele reintroduirait
34
+ // exactement la retrogradation par la taille condamnee ci-dessus.
19
35
  //
20
36
  // The dependency on native-tiers is intentional and one-directional: dispatch
21
37
  // CONSUMES the tier source of truth, it does not duplicate it. native-tiers is a
@@ -25,10 +41,9 @@ import { classifyLeaf, tierFor, TIER_MODEL, LEAF_TYPES } from './native-tiers.js
25
41
  const VALID_NATURES = new Set(Object.values(LEAF_TYPES));
26
42
 
27
43
  export function dispatch({ task, complexity, parallelizable, nature } = {}) {
28
- const score =
29
- typeof complexity === 'number'
30
- ? complexity
31
- : Math.min(100, Math.floor((task?.length || 0) / 10));
44
+ const declaree = typeof complexity === 'number' && Number.isFinite(complexity);
45
+ const score = declaree ? complexity : Math.min(100, Math.floor((task?.length || 0) / 10));
46
+ const complexitySource = declaree ? COMPLEXITY_SOURCES.DECLARED : COMPLEXITY_SOURCES.ESTIMATED;
32
47
  const isPar = parallelizable === true;
33
48
 
34
49
  // Axis 1 — strategy (where). Scalar, as before, minus the fused model suffix.
@@ -47,28 +62,38 @@ export function dispatch({ task, complexity, parallelizable, nature } = {}) {
47
62
  strategyReason = `score ${score} >= 40: heavy, kept in the main thread`;
48
63
  }
49
64
 
50
- // Axis 2 — tier (which model). By nature, via native-tiers. An explicit, valid
51
- // nature wins; otherwise classify the task text. An unknown nature falls back to
52
- // classification rather than guessing, and classification's own default is
53
- // IMPLEMENTATION (deep), so the conservative path is the worst case — protected
54
- // work is never downgraded on a miss.
65
+ // Axis 2 — tier (which model). Croisement nature x complexite, via native-tiers.
66
+ // An explicit, valid nature wins; otherwise classify the task text. An unknown
67
+ // nature falls back to classification rather than guessing, and classification's
68
+ // own default is IMPLEMENTATION (protege), so the conservative path is the worst
69
+ // case — protected work is never downgraded on a miss.
55
70
  const leafType = VALID_NATURES.has(nature) ? nature : classifyLeaf({ label: task || '' });
56
- const tier = tierFor(leafType);
57
- const model = TIER_MODEL[tier]; // 'haiku' (exploration) or null (every other nature -> inherit session model). tierFor never auto-picks balanced/'sonnet'.
71
+ const model = modelForTask({ nature: leafType, complexity: score, source: complexitySource });
72
+ // Le niveau annonce decrit le modele REELLEMENT choisi, pas la classe theorique
73
+ // de la nature : sinon un verdict se contredit (tier deep + modele haiku).
74
+ const tier = tierOfModel(model);
58
75
 
59
76
  const tierReason =
60
- model === null
61
- ? `nature=${leafType} -> ${tier}: inherit the session model (protected, not downgraded)`
62
- : `nature=${leafType} -> ${tier}: ${model}`;
77
+ complexitySource === COMPLEXITY_SOURCES.ESTIMATED
78
+ ? `nature=${leafType}, complexite non declaree (score ${score} devine sur la longueur du prompt) -> ${tier}: ${model === null ? 'modele de session' : model}. Passe \`complexity\` (0-100) pour activer l'echelle par complexite`
79
+ : `nature=${leafType} + complexite declaree ${score} -> ${tier}: ${model === null ? 'modele de session' : model}${
80
+ leafType === LEAF_TYPES.VERIFICATION
81
+ ? ' (ligne rouge: une verification ne descend jamais)'
82
+ : score >= PROTECTED_FLOOR_COMPLEXITY
83
+ ? ' (au-dessus du plancher anti-retrogradation)'
84
+ : ''
85
+ }`;
63
86
 
64
87
  // model applies to a DELEGATED strategy (subagent / mcp-worker leaf); for a
65
88
  // main-thread strategy the work runs on the session model and model is advisory.
66
89
  return {
67
90
  score,
91
+ complexitySource,
68
92
  strategy,
69
93
  nature: leafType,
70
94
  tier,
71
95
  model,
96
+ effort: effortForTask({ nature: leafType, complexity: score, source: complexitySource }),
72
97
  parallelizable: isPar,
73
98
  reasoning: `${strategyReason}. ${tierReason}.`,
74
99
  };
@@ -81,6 +106,12 @@ export function dispatch({ task, complexity, parallelizable, nature } = {}) {
81
106
  // script's concern, only WHICH model each deserves is answered. An explicit
82
107
  // valid nature wins; otherwise the label classifies; a miss stays protected
83
108
  // (implementation -> deep -> null), same conservative path as dispatch().
109
+ //
110
+ // Pas de croisement avec la complexite ici, volontairement : une feuille de
111
+ // script n'en porte pas (le linter et le garde-fou d'ecriture ne voient qu'un
112
+ // libelle), et inventer un score par feuille rouvrirait la retrogradation par la
113
+ // taille. Le croisement appartient au dispatch de TACHE, ou un appelant peut
114
+ // declarer la complexite.
84
115
  export function dispatchBatch(leaves) {
85
116
  if (!Array.isArray(leaves)) return [];
86
117
  return leaves.map((leaf) => {
@@ -0,0 +1,75 @@
1
+ /**
2
+ * L'accuse de reception des outils d'ECRITURE du cycle FD.
3
+ *
4
+ * POURQUOI, ET C'EST MESURE. Le 2026-09-01, sur une seule session :
5
+ * byan_fd_update et byan_fd_advance ont rendu 43 130 jetons pour 17 appels,
6
+ * soit 38 % de tout ce qui est revenu d'un outil ce jour-la. Chaque ecriture
7
+ * renvoyait l'etat ENTIER — backlog redige, idees brutes, table de dispatch,
8
+ * notes, historique des phases — et en plus mis en forme sur deux espaces.
9
+ *
10
+ * Or celui qui ecrit vient de composer cet etat : le lui relire en entier ne
11
+ * lui apprend rien. Ce qu'il a besoin de savoir tient en trois lignes : ou en
12
+ * est le cycle, combien d'items restent, et ce qui vient de changer.
13
+ *
14
+ * LE FICHIER RESTE LA SOURCE DE VERITE. _byan-output/fd-state.json est ecrit
15
+ * avant que l'accuse parte ; qui veut l'etat complet appelle byan_fd_status,
16
+ * dont c'est precisement le metier. On ne perd donc aucune information — on
17
+ * cesse seulement de la repeter a quelqu'un qui vient de la donner.
18
+ *
19
+ * CE QUE L'ACCUSE NE DOIT SURTOUT PAS ETRE. Un etat d'apparence complete.
20
+ * leantime-fd-sync.js lit l'etat depuis le retour de l'outil, avec un repli
21
+ * sur le fichier ; sa garde parseFdState exige donc un backlog, pas seulement
22
+ * une phase. Un accuse est reconnu comme tel et declenche le repli.
23
+ */
24
+
25
+ const STATUTS = ['pending', 'building', 'done', 'skipped'];
26
+
27
+ function compterBacklog(backlog) {
28
+ const out = { total: 0 };
29
+ for (const s of STATUTS) out[s] = 0;
30
+ if (!Array.isArray(backlog)) return out;
31
+ out.total = backlog.length;
32
+ for (const item of backlog) {
33
+ const s = item && typeof item.status === 'string' ? item.status : null;
34
+ if (s && Object.prototype.hasOwnProperty.call(out, s)) out[s] += 1;
35
+ }
36
+ return out;
37
+ }
38
+
39
+ /**
40
+ * Construit l'accuse a partir de l'etat qui vient d'etre ecrit.
41
+ *
42
+ * @param {object} state l'etat FD complet, tel qu'il vient d'etre persiste
43
+ * @param {object} [extra] { changed?: string[], from?: string, note?: string }
44
+ * @returns {object} un accuse compact, jamais une exception
45
+ */
46
+ export function fdAck(state, extra = {}) {
47
+ const s = state && typeof state === 'object' && !Array.isArray(state) ? state : {};
48
+ const e = extra && typeof extra === 'object' ? extra : {};
49
+
50
+ const ack = {
51
+ ok: true,
52
+ fd_id: s.fd_id ?? null,
53
+ feature_name: s.feature_name ?? null,
54
+ phase: typeof s.phase === 'string' ? s.phase : null,
55
+ backlog: compterBacklog(s.backlog),
56
+ // Des COMPTES, pas le contenu : c'est le contenu qui pesait.
57
+ counts: {
58
+ raw_ideas: Array.isArray(s.raw_ideas) ? s.raw_ideas.length : 0,
59
+ notes: Array.isArray(s.notes) ? s.notes.length : 0,
60
+ dispatch_table: Array.isArray(s.dispatch_table) ? s.dispatch_table.length : 0,
61
+ commits: Array.isArray(s.commits) ? s.commits.length : 0,
62
+ },
63
+ // Ou lire l'etat complet quand on en a vraiment besoin. Le dire dans
64
+ // l'accuse evite d'avoir a s'en souvenir.
65
+ state_file: '_byan-output/fd-state.json',
66
+ };
67
+
68
+ if (Array.isArray(e.changed) && e.changed.length) ack.changed = e.changed;
69
+ if (typeof e.from === 'string' && e.from) ack.from = e.from;
70
+ if (typeof e.note === 'string' && e.note) ack.note = e.note;
71
+
72
+ return ack;
73
+ }
74
+
75
+ export default { fdAck };
@@ -47,7 +47,18 @@ export function parseFdState(toolResponse) {
47
47
  return null;
48
48
  }
49
49
  }
50
- if (candidate && typeof candidate === 'object' && typeof candidate.phase === 'string') {
50
+ // UNE PHASE NE SUFFIT PAS A FAIRE UN ETAT. Depuis que les outils d'ecriture
51
+ // FD rendent un accuse compact au lieu de l'etat entier (lib/fd-ack.js, ne
52
+ // du constat que ces retours pesaient 38 % de tout ce qui revenait d'un
53
+ // outil), un objet peut porter une `phase` sans etre un etat. L'accepter
54
+ // ferait travailler decideActions sur un backlog absent, au lieu de retomber
55
+ // sur le fichier, qui est la source de verite. On exige donc le backlog.
56
+ if (
57
+ candidate &&
58
+ typeof candidate === 'object' &&
59
+ typeof candidate.phase === 'string' &&
60
+ Array.isArray(candidate.backlog)
61
+ ) {
51
62
  return candidate;
52
63
  }
53
64
  return null;
@@ -91,8 +91,16 @@ export const DEEP_PREFIX = 'deep-';
91
91
  // check/review/gate/audit/assert/lint; a leaf that runs tests is labelled
92
92
  // 'verify-*' in practice.
93
93
  const VERIFICATION_KEYWORDS = ['verify', 'validate', 'check', 'assert', 'gate', 'lint', 'audit', 'review'];
94
- const ANALYSIS_KEYWORDS = ['analy', 'design', 'architect', 'assess', 'evaluate', 'strategy', 'risk', 'nfr', 'recommend', 'judge', 'score', 'coverage', 'synthes'];
95
- const IMPLEMENTATION_KEYWORDS = ['implement', 'build', 'write', 'generate', 'create', 'dev', 'rgr', 'refactor', 'fix', 'scaffold', 'save', 'optimize', 'aggregate', 'report', 'present', 'plan', 'map', 'select', 'subprocess', 'sub-'];
94
+ // 'concev'/'concept' : le jumeau francais de 'design'. Sans lui, "Concevoir un
95
+ // moteur ... avec detection de cycles" tombait sur 'detect' et repartait en
96
+ // exploration, donc sur le bas de gamme — une tache de conception retrogradee
97
+ // par un mot du milieu de phrase.
98
+ const ANALYSIS_KEYWORDS = ['analy', 'design', 'architect', 'assess', 'evaluate', 'strategy', 'risk', 'nfr', 'recommend', 'judge', 'score', 'coverage', 'synthes', 'concev', 'concept'];
99
+ // Meme raison cote fabrication : "corriger la liste des dependances" contient
100
+ // 'list' et repartait en exploration. La priorite protege-d-abord ne joue que
101
+ // si le vocabulaire protege existe aussi en francais. Les radicaux couvrent
102
+ // les conjugaisons courantes (corrige, correction, redaction...).
103
+ const IMPLEMENTATION_KEYWORDS = ['implement', 'build', 'write', 'generate', 'create', 'dev', 'rgr', 'refactor', 'fix', 'scaffold', 'save', 'optimize', 'aggregate', 'report', 'present', 'plan', 'map', 'select', 'subprocess', 'sub-', 'ecrire', 'redig', 'corrig', 'construi', 'developp', 'ajout', 'modifi'];
96
104
  // cartograph/inventaire/recensement... : vocabulaire de cartographie de
97
105
  // l'existant (lecture + synthese), francais inclus — cas terrain byan_web ou
98
106
  // des feuilles de scan DISCOVERY non reconnues heritaient d'une session Fable 5.
@@ -171,3 +179,107 @@ export function isKnownTierModel(modelId) {
171
179
  export function isDowngradeModel(modelId) {
172
180
  return modelId === TIER_MODEL.cheap || modelId === TIER_MODEL.balanced;
173
181
  }
182
+
183
+ // =========================================================================
184
+ // Croisement NATURE x COMPLEXITE (surface "dispatch de tache")
185
+ // =========================================================================
186
+ // Tout ce qui precede repond a "quel modele merite cette FEUILLE de workflow",
187
+ // une question sans complexite : le linter lit un script statique, il n'a qu'un
188
+ // libelle. Ce qui suit repond a une question differente : "quel modele merite
189
+ // cette TACHE", quand un score de complexite existe. Meme taxonomie de natures,
190
+ // deux surfaces. La nature seule ne peut pas trancher : elle dit combien de
191
+ // jugement le travail porte, pas s'il est dur. Un hello world et un moteur de
192
+ // resolution de dependances sont tous deux de l'implementation ; les router sur
193
+ // le meme modele est le defaut que ce bloc corrige.
194
+ //
195
+ // L'echelle par complexite n'est PAS redefinie ici : elle vit dans
196
+ // dispatch-router.js (claudeModelForComplexity) et ce module la consomme. Deux
197
+ // echelles finiraient par diverger — exactement le defaut qu'on corrige.
198
+ export { claudeModelForComplexity as modelForComplexity } from './dispatch-router.js';
199
+ import { claudeModelForComplexity, claudeEffortForModel } from './dispatch-router.js';
200
+
201
+ // Provenance du score de complexite. DECLARED = un appelant l'a juge et fourni.
202
+ // ESTIMATED = personne ne l'a juge, il a ete devine (longueur du prompt chez
203
+ // dispatch()). La distinction porte une decision : la longueur d'un prompt dit
204
+ // long ou court, jamais facile ou difficile ("refactor auth" tient en treize
205
+ // caracteres), donc un score estime n'autorise ni retrogradation ni montee en
206
+ // gamme d'un travail protege.
207
+ export const COMPLEXITY_SOURCES = Object.freeze({ DECLARED: 'declared', ESTIMATED: 'estimated' });
208
+
209
+ // Seuil du plancher anti-retrogradation (B2). Au-dessus, une implementation est
210
+ // tenue pour complexe : son modele ne peut plus etre un modele de repli. En
211
+ // dessous, l'echelle a le droit de descendre — une tache simple n'a pas besoin
212
+ // du modele le plus cher, et c'est tout l'objet du chantier.
213
+ // 67 est precisement le barreau ou l'echelle elle-meme cesse de descendre
214
+ // (opus) : les deux regles se rejoignent au lieu de se contredire, et le
215
+ // plancher reste un garde-fou si l'echelle bougeait un jour.
216
+ export const PROTECTED_FLOOR_COMPLEXITY = 67;
217
+
218
+ // applyProtectedFloor(model, complexity) -> le modele apres plancher.
219
+ // Le plancher ne fait qu'une chose : au-dessus du seuil, refuser un modele de
220
+ // repli et rendre null (herite du modele de session). Il ne descend jamais rien
221
+ // et ne bloque pas une montee en gamme.
222
+ export function applyProtectedFloor(model, complexity) {
223
+ if (!(complexity >= PROTECTED_FLOOR_COMPLEXITY)) return model;
224
+ return isDowngradeModel(model) ? null : model;
225
+ }
226
+
227
+ // tierOfModel(model) -> le niveau annonce pour un modele effectivement choisi.
228
+ // Necessaire depuis que le modele ne sort plus de la seule nature : le champ
229
+ // "tier" doit decrire ce qui a ete decide, pas la classe theorique de la nature,
230
+ // sinon un verdict se contredit lui-meme (tier deep + modele haiku). Les modeles
231
+ // de haut de gamme retombent sur deep : deep veut dire "rien n'a ete retrograde".
232
+ export function tierOfModel(model) {
233
+ if (model === TIER_MODEL.cheap) return TIERS.CHEAP;
234
+ if (model === TIER_MODEL.balanced) return TIERS.BALANCED;
235
+ return TIERS.DEEP;
236
+ }
237
+
238
+ function clampComplexity(complexity) {
239
+ return Math.min(100, Math.max(0, complexity));
240
+ }
241
+
242
+ // modelForTask({ nature, complexity, source }) -> le modele a passer (chaine) ou
243
+ // null (herite du modele de session). C'est le croisement des deux axes.
244
+ //
245
+ // La nature decide COMMENT la complexite a le droit de jouer :
246
+ // verification : elle ne joue pas. Ligne rouge — un controleur qu'on
247
+ // retrograde ne controle plus rien, et on ne le pin pas non
248
+ // plus en gamme : il reste sur le modele de session, comme
249
+ // dans le script byan-auto-dispatch.
250
+ // mechanique : elle ne joue pas non plus. La nature est declaree sans
251
+ // jugement (un JSON parse ou ne parse pas) ; sa difficulte
252
+ // annoncee ne change pas ce fait.
253
+ // exploration : l'echelle joue, plafonnee a sonnet. Un scan enorme merite
254
+ // mieux que le bas de gamme, jamais le haut de gamme.
255
+ // analyse : l'echelle joue, plancher a sonnet (le linter refuse haiku
256
+ // sur une analyse) et montee en gamme permise.
257
+ // implementation: l'echelle joue en entier, sous le plancher conditionnel.
258
+ export function modelForTask({ nature, complexity, source = COMPLEXITY_SOURCES.ESTIMATED } = {}) {
259
+ if (nature === LEAF_TYPES.VERIFICATION) return null;
260
+ if (nature === LEAF_TYPES.MECHANICAL) return TIER_MODEL[TIERS.BALANCED];
261
+
262
+ const tierDeLaNature = tierFor(nature);
263
+ // Sans score exploitable, ou avec un score seulement devine, on s'en tient a
264
+ // la nature : c'est le comportement d'avant, et il est protecteur.
265
+ if (!Number.isFinite(complexity) || source !== COMPLEXITY_SOURCES.DECLARED) {
266
+ return TIER_MODEL[tierDeLaNature];
267
+ }
268
+
269
+ const cx = clampComplexity(complexity);
270
+ const echelon = claudeModelForComplexity(cx);
271
+
272
+ if (nature === LEAF_TYPES.EXPLORATION) {
273
+ return isUpTierModel(echelon) ? TIER_MODEL[TIERS.BALANCED] : echelon;
274
+ }
275
+ if (nature === LEAF_TYPES.ANALYSIS) {
276
+ return echelon === TIER_MODEL.cheap ? TIER_MODEL[TIERS.BALANCED] : echelon;
277
+ }
278
+ return applyProtectedFloor(echelon, cx);
279
+ }
280
+
281
+ // The effective model includes nature floors/caps and the verification guard.
282
+ // Deriving effort afterwards keeps both knobs on the same rung.
283
+ export function effortForTask(task) {
284
+ return claudeEffortForModel(modelForTask(task));
285
+ }