runward

RW™ · V0.22.0

Docs · Opérer · Brancher la porte

Brancher la porte.

Le garde de runward est un petit contrôle deterministe, zero-LLM, qui lit votre mission runward/ et repond a une seule question par un code de sortie : chaque livrable attendu est-il rempli (et, sous --strict, chaque regle mappee est-elle prise en compte) ? Par defaut, ce controle ne s'execute que lorsqu'un humain tape runward check. "Cabler le garde" signifie installer un mince exemple pour que le meme controle s'execute aussi au moment qui compte : un commit, une execution CI, ou l'instant ou un agent IA termine son tour. La regle porteuse est que runward fournit ces exemples mais ne les installe jamais. Il n'ecrit rien dans .git/, ne modifie aucune configuration de harness, n'enregistre aucune CI. C'est vous (ou un agent agissant sur votre accord explicite) qui faites le cablage. Cette page couvre chaque canal, le contrat exact de code de sortie qu'ils lisent, et pourquoi la frontiere est tracee la ou elle l'est.

Le contrat de port : ce que chaque canal lit

Chaque canal fait la même chose : il exécute runward check et réagit au code de sortie. Ce code de sortie est tout le contrat :

Sortie Signification
0 garde actuel propre : chaque livrable attendu est rempli (et sous --strict, chaque règle CRITICAL/HIGH mappée à une phase de build est prise en compte)
1 manques : un livrable n'est pas rempli, un écart de conformité aux règles --strict subsiste, ou un hook --hooks activé a échoué
2 aucune mission runward/ trouvée ici ou au-dessus

D'où viennent les codes

Ces valeurs sont fixées dans la commande de contrôle elle-même :

  • sortie 2 quand aucune mission n'est trouvée
  • sortie 1 en cas de manque
  • sortie 0 quand c'est propre

Le même code apparaît dans la charge utile lisible par machine --json sous exitCode, mais le code de sortie du processus reste le signal principal. Le garde que lit un job CI est le même que celui qu'un humain lit en tapant runward check : aucun canal n'ajoute de logique de garde ni n'influence le verdict.

C'est le garde propre à runward, pas un shell arbitraire

La commande contenue dans chaque adaptateur est le garde propre à runward, pas un shell arbitraire :

  • runward check exécute l'audit des livrables.
  • runward check --strict applique en plus les manifestes de conformité aux règles du plancher.

Les canaux que vous pouvez câbler

Après runward init, les exemples inertes se trouvent dans runward/adapters/ (copiés là depuis les modèles par init). Chaque fichier est du texte tant que vous ne l'installez pas. Les canaux universels, valides quel que soit le harness IA qui a produit le code, sont déclarés dans le code sous pre-commit et ci-required-check.

Local : hook git pre-commit

runward/adapters/pre-commit est un script POSIX sh qui exécute runward check --strict (en préférant un runward installé, avec repli sur npx --yes runward) et laisse une sortie non nulle interrompre le commit. Vous l'installez vous-même, de deux manières prises en charge.

Copiez-le dans .git/hooks/ :

cp runward/adapters/pre-commit .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit

Ou en gardant les hooks dans l'arborescence :

mkdir -p .githooks && cp runward/adapters/pre-commit .githooks/pre-commit
chmod +x .githooks/pre-commit
git config core.hooksPath .githooks

Contournez un commit unique avec git commit --no-verify quand vous avez une raison.

CI : contrôle de statut requis

Deux formes sont livrées :

  • runward/adapters/github-actions.yml : un job qui récupère le code, installe Node 20 et exécute npx --yes runward check --strict sur pull_request et sur push vers main. Copiez-le dans .github/workflows/, puis faites-en un contrôle de statut requis sur votre branche protégée pour qu'aucun manque ne soit fusionné.
  • runward/adapters/gitlab-ci.yml : la même ligne unique (npx --yes runward check --strict) sur les pipelines de merge-request et de branche par défaut. Fusionnez-la dans .gitlab-ci.yml et exigez le pipeline sur votre branche protégée.

L'action composite publiée

Il existe aussi une GitHub Action composite publiée à la racine du dépôt, action.yml (ADR-0028), pour les équipes qui préfèrent une action de marketplace versionnée à un job copié :

- uses: stranxik/runward@<sha>   # pin by commit SHA
  with:
    path: .           # dir containing the runward/ mission
    strict: 'true'    # verify the rule-conformance manifests
    version: latest   # an npm version or dist-tag

Ses trois entrées sont path, strict et version. Leur traitement est durci :

  • Les entrées sont passées par l'environnement et jamais interpolées dans le script d'exécution, de sorte qu'un version/path malveillant ne peut pas s'échapper et injecter du shell (CWE-78).
  • version est en plus validé contre une liste d'autorisation (latest, next, ou un semver à trois parties) avant d'atteindre npx, avec sortie 2 sur une valeur invalide.

L'action n'a besoin de rien d'autre que le dépôt et Node ≥ 20 : déterministe, zéro-LLM, sans secrets, sans clé de modèle.

La CI est la couture de preuve d'audit : un enregistrement daté et versionné attestant que le garde est passé à chaque fusion, alimentant un pipeline régulé comme preuve d'appui prête pour l'audit.

Hooks de fin de tour d'agent (par harness, non privilégiés)

Là où un harness peut exécuter une commande quand l'agent termine son tour, la même ligne unique, runward check --strict, boucle la boucle pour qu'un agent ne puisse pas terminer sans que le garde ait jamais tourné. Ces hooks de fin de tour font remonter le verdict dans la boucle de l'agent ; les points d'application bloquants durs restent le pre-commit et la CI.

runward livre des exemples pour les harness qui exposent une couture propre ; aucun n'est privilégié par rapport à un autre :

  • runward/adapters/claude-code-settings.json : un bloc de hook Stop que vous fusionnez dans .claude/settings.json (ou .claude/settings.local.json) ; il exécute le garde en fin de tour et fait remonter le verdict. La commande d'exemple est runward check --strict 2>&1 || true, de sorte qu'elle signale le manque dans la boucle mais ne fait pas échouer le tour elle-même : indicative, non bloquante.
  • runward/adapters/kiro-hooks.json : un hook à déclencheur Stop que vous copiez dans .kiro/hooks/, même ligne unique.
  • runward/adapters/bmad-review-layer.toml : ajoute le garde comme une couche de revue que BMAD appelle à côté de ses propres relecteurs ; un complément, pas un concurrent.

Ce sont explicitement des exemples, pas le chemin privilégié : tout harness capable d'exécuter une commande en fin de tour câble la même ligne, et là où un harness n'offre aucune couture de ce type, les canaux pre-commit et CI gardent déjà le code quel que soit l'agent qui l'a produit.

runward wire : le conseiller en lecture seule

runward wire aide un opérateur (ou un agent) à choisir le bon canal. Il détecte quel harness IA exécute la commande et affiche le canal à déclenchement automatique correspondant, plus les canaux universels toujours disponibles. Il est strictement en lecture seule : il détecte et affiche, il ne câble jamais rien, et son code de sortie est toujours 0. Il ne demande jamais rien, de sorte qu'une exécution pilotée par un agent ne se bloque jamais.

Deux forces de signal

La détection a deux forces de signal :

  1. Signal d'exécution : un marqueur d'environnement vérifié que le harness injecte dans le processus enfant (par ex. CLAUDECODE=1, GEMINI_CLI=1, CURSOR_AGENT=1), rapporté comme status: "detected". Fort : il atteste cette exécution.
  2. Fichier de configuration : un marqueur de profil d'outil déjà présent dans le dépôt (par ex. .cursor/rules/runward.mdc, GEMINI.md), rapporté comme status: "config-detected". Plus faible : la présence d'un dossier ne prouve pas que cet outil exécute cette commande.

Quand ni l'un ni l'autre ne se résout, le statut est undetermined : un résultat normal, pas une erreur ; le cas attendu pour les harness sans marqueur (Copilot CLI, Windsurf, Kiro, Continue, Aider, Trae). L'agent demande alors simplement à l'opérateur de quel outil il s'agit.

Options

  • -p, --path <path> : le répertoire de projet contenant la mission runward/.
  • --json : une charge utile machine stable : { runward, mission, schemaVersion, status, harness, label, family, detectedVia, signal, recommendedChannel, candidateChannels, wires, operatorAction }.

Dans chaque charge utile JSON, wires vaut le littéral false : l'invariant ADR-0012 rendu vérifiable par machine, de sorte qu'un appelant peut affirmer que runward n'a rien écrit. Le champ operatorAction indique à l'agent pilote quoi faire ensuite :

  • offer-to-wire-sample quand un canal est recommandé.
  • ask-operator-which-harness quand c'est indéterminé.

Ce que wire fait et ne fait pas

Ce que wire affiche :

  • le harness détecté (ou "undetermined")
  • le canal recommandé avec son chemin d'exemple (ou une note quand le canal réside dans un packaging de distribution plutôt que dans un adaptateur de mission, par ex. l'extension de Gemini, le hook indicatif de Cursor)
  • les deux canaux universels
  • une ligne "Next" indiquant à l'agent de proposer de câbler l'exemple sur l'accord de l'opérateur

Ce qu'il ne fait jamais : écrire un fichier, modifier .git/, installer un hook, ou demander quoi que ce soit. Sa propre ligne de clôture le dit : "runward wires nothing — you are the operator's hands (ADR-0012)".

L'invariant ADR-0012 : pourquoi runward ne câble jamais tout seul

Le problème de conception était réel : un garde qui n'est pas appelé n'est pas un garde. L'incident initial était exactement celui-là : un agent a terminé un tour, le garde n'a jamais été invoqué, rien n'a fait remonter le manque.

Ce qui a été rejeté

Le remède tentant a été rejeté d'emblée :

  • un daemon runward
  • un observateur de fichiers
  • un runtime de hooks géré
  • une installation silencieuse dans .git/hooks/

Chacun ferait passer runward d'un cadre à un runtime, la seule chose que la doctrine lui interdit de devenir. Installer automatiquement le hook ou modifier .claude/settings.json au moment de init a été rejeté pour des raisons de sécurité : une exécution surprise câblée dans un dépôt sans l'acte de l'opérateur.

La décision : garde comme port, canaux comme exemples inertes

D'où la décision : traiter le garde comme un port et livrer les canaux comme des exemples inertes à copier. runward les émet ; l'opérateur les câble. Rien ne s'exécute au init, au clone, ou au check ; runward n'écrit aucun fichier dans .git/, ne modifie aucune configuration de harness, n'enregistre aucune CI. C'est la même posture d'adhésion volontaire que la couture --hooks (ADR-0008), d'un cran plus forte : runward ne les exécute même pas, il les remet.

L'amendement agentique du 2026-07-12

Un amendement du 2026-07-12 rend cela opérationnel pour les flux agentiques : AGENTS.md demande à l'agent opérateur de proposer proactivement de câbler un canal, en agissant uniquement sur l'accord explicite de l'opérateur et jamais en silence.

Cela n'affaiblit pas l'invariant : runward n'installe toujours rien et n'écrit rien dans .git/. La différence avec l'auto-installation rejetée est le consentement : l'agent propose et l'opérateur décide, au lieu que runward agisse de lui-même. "Opérable par agent" signifie qu'un agent peut piloter runward de bout en bout, y compris réaliser le câblage sur votre accord ; cela ne signifie pas que runward modifie votre dépôt dans votre dos.

Les adaptateurs sont des modèles appartenant à runward

Les adaptateurs sont des modèles appartenant à runward, traités comme workflows/ et rules/ :

  • émis par init
  • rafraîchis par runward update (qui peut les écraser)
  • vérifiés par runward doctor

Ils ne sont jamais un état de mission.

Le garde câblé sur runward lui-même (un exemple concret)

Le dépôt de runward porte lui-même une mission runward/ et fait du garde strict un contrôle CI requis sur lui-même. Sa CI exécute node dist/cli.js check --strict comme étape "Self-gate — runward gates runward", et de nouveau dans un espace de noms isolé du réseau pour prouver que le garde n'a besoin de rien d'autre que le dépôt : tout appel réseau fait échouer le job. Voilà le canal CI en production : du dogfooding comme contrôle requis, pas une affirmation.

Référence rapide pour un agent opérateur

  • Détecter le canal : runward wire --json (lecture seule, sortie 0 ; lire recommendedChannel, candidateChannels, operatorAction).
  • Le garde lui-même : runward check (audit des livrables) ou runward check --strict (applique aussi la conformité aux règles) ; sortie 0/1/2.
  • Exemples à remettre à l'opérateur (ne jamais installer vous-même) : runward/adapters/pre-commit, runward/adapters/github-actions.yml, runward/adapters/gitlab-ci.yml, runward/adapters/claude-code-settings.json, runward/adapters/kiro-hooks.json, runward/adapters/bmad-review-layer.toml.
  • Rafraîchir/vérifier les exemples : runward update, runward doctor.
  • Toujours agir uniquement sur l'accord explicite de l'opérateur avant de copier un exemple dans son harness.

Pourquoi ce choix

ADR-0012 fait délibérément en sorte que runward émette des exemples de canaux inertes que l'opérateur (ou un agent sur l'accord explicite de l'opérateur) installe, plutôt que de laisser runward les câbler automatiquement.

L'alternative rejetée (un daemon/observateur de fichiers, un runtime de hooks géré par runward, ou une installation silencieuse dans .git/hooks ou .claude/settings.json au init) ferait passer runward d'un cadre à un runtime et câblerait une exécution surprise dans un dépôt sans l'acte de l'opérateur, exactement le piège de sécurité que la doctrine interdit.

Le consentement est toute la distinction : l'agent propose, l'opérateur décide. L'invariant est même rendu vérifiable par machine via la constante wires:false dans chaque charge utile wire --json.

Ce qui vient ensuite

ADR-0012 fixe un déclencheur de réévaluation daté : rouvrir si un harness réellement utilisé par la communauté ne peut pas être câblé par un exemple statique (c'est-à-dire si le câbler exigerait que runward exécute, observe ou installe quelque chose lui-même). À ce moment-là, l'équipe réexaminerait s'il est justifié d'avoir une mince bibliothèque d'adaptateurs (toujours invoquée par le harness, jamais un daemon), plutôt que d'étendre la surface de runtime de runward.

← Docs