Contrat exact des sept outils du noyau. Vérifié contre les schémas réels du code
(src/okf_hub/tools/*.py, constante SCHEMA) : si ce document et le code
divergent, le code fait foi et ce fichier est en dette.
Pour la justification de chaque comportement, voir la spécification :
SPEC-okf-bundle-hub-v0.md § 5.
Toujours le champ name du manifeste, jamais le nom du répertoire dans
bases/. Les deux diffèrent dès qu'un clone est renommé. kb_list donne la
correspondance.
Relatifs à corpus-dir, séparateur /. Un chemin absolu, un .. ou un lien
symbolique sortant du corpus sont refusés (NOT_FOUND).
Une erreur métier retourne un résultat MCP avec isError: true, dont le texte
suit exactement :
ERROR: <CODE>: <message>
| Code | Signification | Que faire |
|---|---|---|
UNKNOWN_BASE |
Base inconnue. Le message liste les bases valides. | Utiliser un nom de la liste. Un re-scan silencieux a déjà été tenté. |
NOT_FOUND |
Document, section ou proposition introuvable. Pour une section, le message liste les headings disponibles. | Corriger le chemin ou la section. |
INVALID_INPUT |
Paramètre manquant ou mal typé, hors bornes, énumération non respectée, regex invalide, retour à la ligne interdit. Pour un paramètre manquant, le message nomme la cause (absent, null, vide, mauvais type) et liste les clés reçues — jamais les valeurs. |
Corriger l'entrée. Ne pas réessayer à l'identique. Si les clés reçues ne sont pas celles envoyées, le défaut est côté client : le signaler à l'opérateur avec le message complet. |
BASE_BUSY |
Verrou d'écriture non acquis en 15 s. | Réessayer plus tard. Ce n'est pas une défaillance. |
IO_ERROR |
Défaillance réelle : ripgrep absent, git en échec, disque. | Signaler à l'opérateur. Un retry n'aidera pas. |
Le SDK MCP ne valide pas les arguments contre inputSchema : un paramètre
manquant ou mal typé atteint l'outil, qui le rend en INVALID_INPUT (isError),
avec la liste des clés reçues. Exemple, pour un appel arrivé sans aucun argument :
ERROR: INVALID_INPUT: paramètre 'base' requis (chaîne non vide) — paramètre 'base' absent ; aucun paramètre reçu
En revanche, un message mal encodé en JSON (par exemple un champ content
dont un guillemet, un antislash ou un saut de ligne n'est pas échappé par le
client) est rejeté par le transport avant d'atteindre le moindre outil : le
client reçoit une erreur JSON-RPC standard, pas un isError. Ce cas n'apparaît
donc dans aucun code kb_* ci-dessus ; côté serveur, le hub le journalise
désormais (transport : message client rejeté avant tout outil…). Un kb_propose
qui « échoue » sans laisser de trace ni de verdict relève de ce cas, pas d'une
validation d'outil : le contenu à corriger est l'échappement JSON côté client,
jamais la contribution elle-même (ARCHITECTURE.md § 5 ter).
kb_search, kb_list et kb_proposal_status plafonnent leur sortie à
~4 000 tokens
(approximation caractères/4, soit 16 000 caractères). La troncature s'arrête sur
un résultat entier — jamais au milieu — et est signalée par une ligne
[résultats tronqués, N élément(s) omis — …].
kb_read n'est pas plafonné : c'est le mode « table des headings » qui joue ce
rôle. kb_governance ne l'est pas non plus, conformément à la spec.
Les descriptions de kb_list, kb_search, kb_read, kb_governance,
kb_propose et kb_proposal_status énumèrent les bases connues avec leur titre et leur objet, et
sont recalculées à chaque tools/list. C'est ce qui permet à une session de
router sa requête vers la bonne base sans appel préalable.
Liste les bases disponibles.
| Paramètre | Type | Défaut | Rôle |
|---|---|---|---|
include_pending_concerns |
boolean |
false |
Ajoute la liste (id, type, concerns) des propositions en attente. |
Sortie — un bloc par base :
## solution-editeur-x — Base solution Éditeur X
Documentation d'exploitation de la solution X : configuration, incidents connus.
version : 1.0.0
documents : 42 | propositions en attente : 3
Avec include_pending_concerns, un second bloc par base suit :
### propositions en attente — solution-editeur-x
- prop-2026-06-14-a3f2 (correction) — procédure de reconnexion SSO
Les listes de concerns sont tronquées en priorité : les résumés de bases
sont toujours émis en entier, les concerns seulement si le budget le permet.
Sans aucune base enregistrée, la sortie explique comment en importer une.
Quand l'appeler — au début d'une session, pour savoir quoi interroger ; et
avec include_pending_concerns avant kb_propose, pour ne pas soumettre un
doublon. Ce n'est pas obligatoire : la déduplication est le travail du
gestionnaire, pas le vôtre.
Recherche plein texte dans le corpus d'une ou plusieurs bases, via ripgrep.
| Paramètre | Type | Défaut | Rôle |
|---|---|---|---|
base |
string | string[] |
requis | Nom de la base, liste de noms, ou "*" pour toutes les bases enregistrées. Voir « Multi-bases » ci-dessous. |
query |
string |
requis | Termes, ou expression régulière en mode regex. |
mode |
"keyword" | "regex" |
"keyword" |
Voir ci-dessous. |
max_results |
integer 1–25 |
8 |
Hors bornes → INVALID_INPUT. Plafond global : voir « Multi-bases ». |
Mode keyword — les termes sont séparés par des espaces et traités comme
des littéraux (pas de regex : 3.2(beta) cherche bien 3.2(beta)).
- ET strict d'abord : seuls les documents touchant tous les termes, insensible à la casse.
- Si zéro résultat, repli automatique en OU, classé par nombre de termes
touchés puis densité, avec la mention explicite :
[aucun document ne contient tous les termes — résultats partiels].
Mode regex — dialecte ripgrep (syntaxe Rust regex). Une expression
invalide remonte INVALID_INPUT avec le message de ripgrep tel quel.
Sortie — par résultat :
### procedures/sso.md — Procédure de reconnexion SSO
frontmatter : {title: Procédure de reconnexion SSO, tags: [sso], last-verified: 2026-01-15}
L12-16 § reconnexion
| ## Reconnexion
|
| Cliquer sur « réauthentifier » dans le menu profil.
pathrelatif au corpus, puis le titre (frontmattertitle, sinon premier#) ;- frontmatter limité à
title, dates ettags— le reste n'est pas exposé ; - extraits : ligne touchée ± 2 lignes, 3 au maximum par document, fenêtres qui se recouvrent fusionnées ;
- après
§, le heading de la section contenant la ligne touchée, sous sa forme normalisée — ou(préambule)si la ligne précède tout heading.
Le § est fait pour être recopié. C'est exactement ce que kb_read attend
dans section :
kb_search base=… query="réauthentifier" → " L12-16 § reconnexion"
kb_read base=… path=procedures/sso.md section="reconnexion"
Sur un document dépassant read-toc-threshold, cela économise l'aller-retour par
la table des headings. Le heading retenu suit la ligne touchée, pas le début
de la fenêtre de contexte : celle-ci déborde de deux lignes et peut mordre sur la
section précédente.
Déclassement des sommaires — index.md et log.md (noms réservés OKF)
passent derrière tout autre document, et la sortie le signale. Ce sont des
tables de matières ; elles matchent beaucoup et n'apprennent rien. Elles
ressortent quand rien d'autre ne correspond. Écart assumé au § 5.2, voir
ARCHITECTURE.md.
Ce que kb_search n'est pas — un ripgrep est un motif sur le texte brut. Il sert à trouver, jamais à compter : un champ de frontmatter ou une section cités entre backticks dans un document de conventions seront comptés à tort.
Multi-bases (§ 10.3). base accepte, en plus d'un nom unique :
- une liste de noms —
base: ["solution-editeur-x", "okf-hub-guide"]— chacun doit exister, sinonUNKNOWN_BASE(comme pour un nom unique) ; aucune recherche n'est lancée si l'un d'eux est invalide. Les doublons sont silencieusement dédoublonnés. base: "*"— toutes les bases actuellement enregistrées. Sans aucune base enregistrée, la sortie l'indique au lieu d'échouer.
max_results reste un plafond de sortie global, jamais un plafond par
base : avec deux bases interrogées et max_results: 8, le total des résultats
rendus est au plus 8, pas 16. Le budget est réparti à parts égales entre
les bases interrogées ; une base qui a moins de résultats que sa part cède son
reliquat aux autres plutôt que de le perdre — la répartition finale peut donc
être inégale si les bases n'ont pas toutes assez de matches.
Relever max_results ne fait jamais réapparaître un résultat masqué par le
plafond de caractères (~4000 tokens, § « Plafond de sortie » ci-dessus). Les
résultats sont ajoutés à la sortie dans l'ordre du classement, et l'arrêt au
premier résultat qui dépasse le budget restant est définitif — tout résultat
suivant est omis, quelle que soit sa taille. Les résultats affichés sont donc
toujours les mieux classés (un préfixe du classement), jamais un mélange
qui aurait sauté un gros résultat pertinent pour faire de la place à un petit
moins pertinent. max_results ne change que la longueur de la liste de
candidats derrière ce plafond — ni le monter ni le descendre ne change les
résultats réellement montrés une fois la troncature atteinte. Le message
[résultats tronqués, N élément(s) omis — …] le rappelle. Seule une requête
reformulée change le classement, donc ce qui arrive en tête.
Avec plus d'une base, la sortie est groupée par base, chacune sous son propre en-tête (même format qu'à une seule base) :
2 résultat(s) dans 2 base(s) pour : réauthentifier
## Base : solution-editeur-x
1 résultat(s) dans 'solution-editeur-x' pour : réauthentifier
### procedures/sso.md — Procédure de reconnexion SSO
L12-16 § reconnexion
| ...
## Base : okf-hub-guide
1 résultat(s) dans 'okf-hub-guide' pour : réauthentifier
### ...
Une base sans aucun résultat n'apparaît pas dans la sortie groupée. Avec un
nom unique (chaîne, pas liste ni "*" à plusieurs bases), la sortie reste
strictement celle d'aujourd'hui — sans en-tête de groupe.
Lit un document, ou une seule de ses sections.
| Paramètre | Type | Défaut | Rôle |
|---|---|---|---|
base |
string |
requis | Nom de la base. |
path |
string |
requis | Chemin relatif au corpus, ou lien interbase <base>:/<chemin> (voir ci-dessous). |
section |
string |
— | Titre du heading à extraire. |
force |
boolean |
false |
Retourne le document entier même s'il est volumineux. |
Lien interbase dans path. Le corps des documents cite parfois une autre
base par un lien littéral [libellé](phoenix-blueway:/chemin.md). kb_read
reconnaît cette forme quand elle est recopiée telle quelle dans path : si le
préfixe avant :/ correspond au nom d'une base enregistrée, c'est cette
base qui est lue, base étant alors ignoré pour l'appel. Un préfixe qui ne
correspond à aucune base connue est traité comme un chemin littéral (donc,
normalement, NOT_FOUND) — pas d'erreur UNKNOWN_BASE surprenante sur un
faux positif.
kb_read base=el2d-referentiel path="el2d-blueway:/environnement/bdd-ext.md"
→ lit environnement/bdd-ext.md dans la base el2d-blueway, pas el2d-referentiel
Trois comportements, dans cet ordre :
sectionfourni → la section seule, du heading jusqu'au prochain heading de niveau inférieur ou égal.- Document >
read-toc-threshold(8192 octets par défaut) et nisectionniforce→ frontmatter + table des headings avec tailles approximatives, au lieu du contenu. - Sinon → le document complet, frontmatter inclus.
Correspondance de section — insensible à la casse, après normalisation
identique du heading et du paramètre : liens [texte](url) → texte, images
→ texte alternatif, backticks supprimés, emphase */**/~~ supprimée, _
supprimé sauf en position intra-mot (kb_read reste kb_read), espaces
réduits.
Donc section: "procedure de reconnexion sso" trouve
## Procédure de \reconnexion` SSO`.
Headings dupliqués — la première occurrence est retournée, avec la
mention [N autres sections portent ce titre].
Section introuvable — NOT_FOUND, et le message liste tous les headings
du document : un second appel suffit à corriger le tir.
Sécurité — résolution canonique du chemin puis vérification d'inclusion
stricte dans corpus-dir. .., chemins absolus et liens symboliques sortants
sont refusés.
Chemin hors corpus mais réel dans le bundle. Si le chemin demandé n'existe
pas sous corpus-dir, mais qu'un fichier existe au même chemin relatif à la
racine du bundle (typiquement un champ de frontmatter comme source_xml,
relatif au bundle et pas à corpus-dir, recopié tel quel dans path), le
NOT_FOUND le signale explicitement au lieu d'un générique « introuvable » —
pour couper court à une tentative de lecture qui ne peut pas aboutir. Ce
diagnostic supplémentaire ne s'applique jamais aux tentatives d'évasion
(.., symlink sortant) : leur message reste inchangé, opaque — un chemin qui
sort de corpus-dir par ce biais garde exactement le refus d'aujourd'hui.
Retourne les règles d'une base.
| Paramètre | Type | Rôle |
|---|---|---|
base |
string |
requis |
Sortie — le contenu intégral de GOVERNANCE.md, suivi de schema.yaml dans
un bloc de code s'il est déclaré, ou de [aucun schema.yaml déclaré pour cette base].
Bandeau de gouvernance en brouillon — si GOVERNANCE.md porte
status: draft dans son frontmatter, la sortie s'ouvre sur :
[GOUVERNANCE EN BROUILLON — les règles peuvent évoluer, les propositions restent acceptées]
Rien n'est bloqué pour autant : c'est un avertissement sur la maturité des
règles, pas une machine à états. Absent ou inconnu, le statut vaut stable.
Quand l'appeler — avant kb_propose, pour savoir ce que la base attend
d'une proposition (sources exigées, seuil de confiance, périmètre). Et
systématiquement au début d'une revue : c'est le contexte du gestionnaire.
Dépose une proposition. Seul outil d'écriture du noyau, confiné à
proposals/pending/. Il ne touche jamais au corpus.
| Paramètre | Type | Contrainte |
|---|---|---|
base |
string |
requis |
type |
"observation" | "correction" | "addition" | "question" |
requis |
concerns |
string |
requis, ≤ 200 car., sans \n ni \r |
content |
string |
requis, ≤ 16 Ko (16384 octets UTF-8) |
sources |
string[] |
requis, 1 à 20 entrées, ≤ 300 car. chacune, sans \n ni \r |
confidence |
"high" | "medium" | "low" |
requis |
submitted_by |
string |
requis, ≤ 100 car., sans \n ni \r |
Sémantique de type
| Valeur | Sens |
|---|---|
observation |
Fait constaté, sans présumer d'un document existant. |
correction |
Contredit un contenu actuel. |
addition |
Complète un sujet déjà couvert. |
question |
Lacune identifiée, sans réponse fournie. Le gestionnaire enquête ou rejette. |
Pourquoi les retours à la ligne sont refusés — concerns, submitted_by et
sources sont injectés dans le sujet et les trailers du message de commit. Un
\n permettrait de forger un faux Reviewed-By: et de corrompre les invariants
d'audit basés sur git log --grep. Le refus est normatif, pas cosmétique.
content n'a aucune restriction de caractères : il vit dans le corps du fichier,
après un frontmatter sérialisé par bibliothèque YAML. Un --- dans le contenu ne
casse rien.
Convention pour submitted_by — convention d'acteur OKF § 7 :
human:<id>, <agent>/<version> (ex. claude-code/opus-5), process:<id>.
Le champ est déclaratif et non authentifié en v0 : il ne doit peser dans
aucune décision d'intégration.
Sortie
Proposition déposée.
id : prop-2026-08-30-a3f2
chemin : proposals/pending/prop-2026-08-30-a3f2.md
base : solution-editeur-x
Elle est en attente de revue par le gestionnaire de la base.
Le corpus n'a pas été modifié.
Effets de bord — sous verrou exclusif de la base : création de
proposals/pending|accepted|rejected avec .gitkeep si absents, ajout de
.okf-hub.lock au .git/info/exclude, écriture atomique du fichier, et un
commit ne contenant que ce fichier :
proposal: prop-2026-08-30-a3f2 (correction) — procédure de reconnexion SSO
Submitted-By: human:morva
Relire le verdict — kb_proposal_status avec l'id retourné ici. La sortie
de kb_propose rappelle l'appel exact. Seul le corps d'une proposition
résolue reste hors MCP : il se relit par accès git direct dans
proposals/accepted|rejected/.
schema.yaml ne s'applique pas ici — il décrit le frontmatter du corpus,
pas celui des propositions. Ne cherchez pas à vous y conformer : soumettez
l'information, sa mise en forme conforme au schéma relève du gestionnaire à
l'intégration. Les champs de cet outil sont le seul format requis.
Consulte l'état et la résolution des propositions. Lecture pure : aucun verrou, aucun état, git reste canonique.
| Paramètre | Type | Défaut | Rôle |
|---|---|---|---|
base |
string |
requis | Nom de la base. |
id |
string |
— | Id exact d'une proposition, tel que retourné par kb_propose. |
submitted_by |
string |
— | Contributeur déclaré. Correspondance exacte, casse ignorée. |
status |
"pending" | "accepted" | "rejected" |
— | Restreint à ce statut. |
limit |
integer 1–50 |
20 |
Les plus récentes d'abord. |
Au moins un de id ou submitted_by est requis — sinon INVALID_INPUT.
Sans filtre, l'outil déverserait proposals/ en entier. status et limit ne
font que raffiner.
Sortie — par proposition :
2 proposition(s) dans 'solution-editeur-x' pour : submitted_by=human:morva
### prop-2026-08-30-a3f2 — accepted
type : correction | concerns : procédure de reconnexion SSO
soumise : 2026-08-30T09:12:00Z par human:morva
résolue : 2026-08-31T14:02:11Z — accepted
integrated-into : procedures/sso.md (lisibles via kb_read)
### prop-2026-08-29-b81c — rejected
type : observation | concerns : bouton déplacé
soumise : 2026-08-29T16:40:00Z par human:morva
résolue : 2026-08-31T14:02:11Z — rejected
rejection-reason : doublon de prop-2026-08-30-a3f2, moins bien sourcé
Tri par submitted-at décroissant ; une proposition sans date passe en fin
de liste. Le corps n'est pas retourné : suivez integrated-into avec
kb_read pour lire ce qui a été intégré.
Le statut vient de l'emplacement du fichier, qui fait foi (§ 6.2). Le champ
status du frontmatter n'est qu'affiché. En cas de divergence, la ligne
[incohérence status/emplacement : … — l'emplacement fait foi] est ajoutée et
l'appel réussit quand même.
Cas limites
| Situation | Comportement |
|---|---|
id introuvable dans les trois répertoires |
NOT_FOUND |
submitted_by sans aucune proposition |
Résultat vide, pas une erreur |
| Frontmatter illisible ou absent | Fichier ignoré, [N fichier(s) illisible(s) ignoré(s)], journalisé |
Plus de résultats que limit |
[N plus ancienne(s) non listée(s) — augmentez limit] |
submitted_by n'est pas authentifié (§ 8). Le filtre retrouve les
propositions déclarées sous ce nom, sans garantie d'identité.
Confinement — cet outil est la seule exception à la liste d'exclusions
transverse du § 5.2, qui retire proposals/ des lectures. Exception limitée à
lui, en lecture seule : un lien symbolique déposé dans pending/ et pointant
hors du bundle est ignoré, comme pour kb_read.
Relance la découverte des bases. Aucun paramètre.
Sortie
Découverte terminée : 2 base(s) enregistrée(s).
ajoutées (1) : phoenix
retirées (0) : —
inchangées (1) : okf-hub-feedback
Bundles invalides (ignorés) :
- brouillon : champ obligatoire manquant : governance.rules
Collisions de name (premier en ordre lexicographique retenu) :
- 'doublon' : 'aaa-clone' retenu, 'zzz-clone' ignoré
Avertissements :
- [phoenix] bundle-spec '9.9' différent de la version supportée '0.1' — chargement tenté et réussi
PORTÉE MONO-INSTANCE. Le rescan n'affecte que la session qui l'appelle — comme toute découverte sur ce hub : chaque client MCP a son instance et son registre, il n'y a ni état partagé ni démon (§ 4.4).
Vous n'avez généralement pas besoin de l'appeler. Deux déclencheurs couvrent déjà le besoin, chacun sous un cooldown de 5 s par instance :
- une erreur
UNKNOWN_BASEdéclenche un re-scan silencieux puis retente l'appel ; - tout
kb_listdéclenche la découverte avant de répondre : une base importée après le démarrage d'une session lui devient donc visible dès qu'elle liste, sans rescan explicite ni redémarrage.
Leurs cooldowns sont comptés séparément : un kb_list ne prive pas l'appel
suivant du re-scan compensatoire sur UNKNOWN_BASE.
Ce qui reste propre à kb_hub_rescan : le rapport ci-dessus — bundles
rejetés avec leur motif, collisions de name, avertissements de compatibilité.
C'est un outil de diagnostic d'import, pas de rafraîchissement.
Répondre à une question depuis les bases
kb_list → quelle base ? (souvent inutile :
les descriptions d'outils listent
déjà les bases)
kb_search base=… query="termes" → quels documents ?
kb_read base=… path=… section=… → le contenu utile, pas le fichier entier
Contribuer un fait nouveau
kb_governance base=… → ce que la base exige
kb_list include_pending_concerns=true → quelqu'un l'a-t-il déjà signalé ?
kb_search base=… query="sujet" → que dit le corpus aujourd'hui ?
kb_propose base=… type=correction … → dépôt. Le corpus n'est pas modifié.
→ note l'`id` retourné.
Relire le verdict d'une proposition — plus tard, éventuellement dans une autre session :
kb_proposal_status base=… id=prop-… → pending / accepted / rejected
kb_read base=… path=<integrated-into> → ce qui a réellement été écrit
Aucun accès git n'est nécessaire côté contributeur pour cette boucle.
Ce qu'une session consommatrice ne fait jamais — écrire dans le corpus,
git commit sur un dépôt de base, ou considérer qu'une proposition déposée est
acquise. Elle ne l'est qu'après revue humaine.
Le rôle gestionnaire n'utilise pas les outils MCP pour écrire : il passe par le
moteur bin/okf-review, qui gère le verrou, les trailers et le commit atomique.
Procédure complète dans ../skills/kb-review/SKILL.md.
okf-review reconcile <base> [--apply] # étape 0, rattrapage des crashs
okf-review context <base> # golden rules + schéma + corpus
okf-review inventory <base> [--full] # propositions en attente
okf-review resolve <base> --plan p.json [--dry-run]Format du plan (resolve) :
{
"summary": "résumé sur une ligne — devient le sujet du commit",
"reviewed_by": "human:<qui confirme>",
"resolutions": [
{ "id": "prop-…", "resolution": "accepted", "integrated_into": ["chemin.md"] },
{ "id": "prop-…", "resolution": "rejected", "reason": "motif obligatoire" }
],
"edits": [
{
"path": "chemin/relatif/au/corpus.md",
"section": "Titre du heading à remplacer",
"content": "## Titre\n\nnouveau contenu",
"frontmatter": { "last-verified": "2026-08-30" }
}
]
}| Champ d'édition | Effet |
|---|---|
content seul |
Remplace tout le fichier. Crée le document s'il n'existe pas. |
content + section |
Remplace cette seule section. À préférer : évite de réécrire un gros document. |
append |
Ajoute en fin de document. Exclusif de content. |
frontmatter |
Fusionne ces champs dans le frontmatter. Combinable avec les précédents. Les champs non cités gardent leur écriture exacte — liste en style flow, timestamp ISO, commentaires, guillemets. Si cette fidélité ne peut pas être garantie, la résolution est refusée (INVALID_INPUT) sans rien modifier : le plan entier est sans effet, et le message dit quoi corriger à la source (voir ARCHITECTURE.md § 5.4). |
Un plan = un commit. Trois groupes indépendants confirmés = trois plans successifs. Le regroupement en un seul commit ne vaut que pour des propositions portant sur le même sujet.
Codes de sortie : 0 succès, 4 erreur métier (ERROR: <code>: … sur
stderr), 5 erreur d'entrée/sortie ou JSON illisible.