HT002 · L'installer sur macOS et le faire tourner¶
2026-09-07, la tâche tient en une phrase : cloner cppide, l'installer sur ce Mac et le faire tourner. Résultat : $38.24 / 4 étapes / environ 1 heure, le programme tourne effectivement sur macOS (PID 96040, Mach-O arm64), et le verdict est inatteignable — remonté à l'humain, qui a choisi de l'accepter.
C'est le premier run doté d'un goal guard — tout ce qui avait été réécrit à partir des leçons de HT001 passait ce jour-là pour la première fois sur la vraie API. Il a évité un piège dans lequel HT001 était tombé, et a en même temps produit un échec neuf, exactement inverse : avoir transformé git clone && make && ./cppide en une heure de travail. La partie la plus utile de cette page, c'est la seconde.
L'enregistrement brut est dans human-test/HT002/, les comptes sont calculés à partir de runs/manifest.json et de runs/sessions.db :
La forme de ce run¶
| Élément | Valeur |
|---|---|
| Tâche | cloner cppide, l'installer et le faire tourner sur ce Mac, l'humain doit pouvoir réellement ouvrir l'interface et s'en servir |
| Coût | $38.24 — 4 étapes, détail ci-dessous |
| Durée | environ 1 heure (total 0.97h) |
| Tours | 70 (somme des num_turns des quatre étapes) |
| Contexte du thread principal | 36.8K → 80.2K, chute brutale aux frontières d'étape, pas une croissance monotone |
| Répartition | 6 subagents ; 7 appels d'outils pour le coordinateur contre 212 pour les subagents |
| Cache | 90.8% de hits |
| Workbench | 5 livrables, 4 notes, 11 fichiers de spill, 0 script réutilisable |
| Résultat | ça tourne (PID 96040, Mach-O arm64) ; verdict inatteignable, l'humain accepte |
| Version | premier run avec goal guard |
Coût, tours et durée de chaque étape¶
| Étape | Coût | Tours (num_turns) | Durée |
|---|---|---|---|
| Clarifier les besoins | $0.5306 | 9 | 0.10h |
| Fixer l'objectif | $0.4117 | 12 | 0.05h |
| Travail | $35.8950 | 12 | 0.73h |
| Travail · verdict #1 | $1.4037 | 37 | 0.09h |
| Total | $38.2409 | 70 | 0.97h |
La colonne des durées est la somme des valeurs arrondies de chaque étape. En recalculant sur l'enregistrement brut, la somme exacte des quatre duration_s de manifest.json vaut 3471.53 secondes = 0.9643h ; cette page conserve 0.97h, l'écart est de 0.006h.
L'argent est presque entièrement parti dans l'étape « travail » — $35.8950, soit 93.9%. La première section explique où est passé cet argent, et pourquoi il n'aurait pas dû partir comme ça.
1. Le gros titre : git clone && make && ./cppide transformé en une heure¶
C'est la découverte la plus importante de ce run, plus importante que tous les « mécanismes qui ont fonctionné » ci-dessous.
À l'étape de définition de l'objectif, le juge s'est écrit 15 critères de verdict (texte original dans 目标.md, sous .flower/notes/). Par nature :
| Catégorie | Nombre | Exemple |
|---|---|---|
| Vérifient réellement « que la chose marche » | environ 5 | build exit 0 ; processus vivant ≥30 secondes ; commande de redémarrage rejouable dans un nouveau shell |
| Vérifient « que le processus a respecté les règles » | 6 | les mtime de ~/.zshrc / .zprofile / .bash_profile sont tous antérieurs à cette session ; aucun brew install / npm -g / sudo n'a été lancé ; la mtime de .flower/ n'a pas changé, rien n'a été ajouté ni supprimé dans runs/ |
| Invérifiables par principe | 4 | prendre une capture d'écran et la regarder ; console devtools ; clic manuel ; confirmation par l'intéressé |
Seul un tiers vérifie « est-ce que cette chose marche vraiment ». Six critères vérifient si le système s'est bien tenu — dont un qui contrôle la mtime du répertoire .flower/, c'est-à-dire le répertoire du framework lui-même.
Le critère qui contrôle .flower/ s'est lui-même mis en échec¶
Ce n'est pas un problème théorique. Au tour de verdict, le juge a honnêtement écrit dans la rubrique « non validé » :
Toutes les mtime des fichiers sous
.flower/sont antérieures au début de la session — échec littéral :INDEX.md16:14,artifacts/01–05(15:40–16:12),notes/决策-项目形态与验收路径.md15:43 ont tous été écrits pendant la session.
La raison : le framework exige impérativement d'écrire les longs livrables dans .flower/artifacts/ et les décisions dans .flower/notes/. Ce critère de verdict exigeait que ce répertoire ne bouge pas. Deux règles en collision frontale, et c'est la checklist de verdict elle-même qui l'a fabriquée — l'exécutant qui n'aurait écrit aucun long livrable aurait été plus conforme.
Il n'a pas essayé de passer en douce : l'étape de travail a signalé d'elle-même ce conflit dans le rapport final, et le journal de décision indique comment il a été traité — aucun fichier existant n'a été touché (需求.md, 目标.md, 问答记录.md : mtime et MD5 identiques avant et après), uniquement des ajouts. Après revérification, le juge a admis que « la falsification que ce critère devait prévenir n'a pas eu lieu », mais il l'a tout de même listé comme non validé, conformément à la règle, et laissé la décision à l'humain.
Cause racine n° 1 : les limites ont été traitées comme des critères de verdict¶
Les limites posées en phase de clarification préalable (« l'installation doit rester dans le répertoire du projet », « ne pas toucher au code métier ») contraignaient la façon de travailler, mais elles ont été transformées en points de contrôle vérifiant le livrable. D'où des critères du type « contrôler la mtime de .zshrc », « contrôler la mtime de .flower/ ».
Une limite ≠ un critère de verdict. La limite contraint le processus, le critère vérifie le résultat. Une fois les deux confondus, chaque limite ajoutée revient à ajouter un critère — et les limites sont précisément ce que la phase de clarification encourage à écrire en abondance : CLARIFIER_RULES dit noir sur blanc « préciser ce qui ne sera pas fait. Ce paragraphe encadrera chacun de ceux qui travailleront ensuite. »
En regardant l'enregistrement brut, on voit chaque maillon de cette amplification : la limite du brief disait « ne pas toucher aux .flower/ et runs/ déjà présents dans le répertoire courant », devenue dans la checklist de verdict « toutes les mtime des fichiers sous .flower/ sont antérieures au début de cette session » — de « ne pas toucher à l'existant » à « aucun nouveau fichier autorisé dans tout le répertoire ».
Cause racine n° 2 : JUDGE_RULES ne pousse que dans un sens — et ça datait du jour même¶
Plus tôt dans la journée, à partir des leçons de HT001, ces phrases avaient été écrites dans JUDGE_RULES :
- « chaque critère doit être vérifiable sur-le-champ », « ce qui manque ou reste vague, tu le complètes »
- « on juge le livrable, pas le code source » (avec le contre-exemple macOS)
- « les critères invérifiables dans l'environnement courant, signale-les sur-le-champ »
- « commence par regarder où tu es (tu peux faire
uname -a) »
Chacune pousse vers plus de contrôles, plus de rigueur. Nulle part dans ces règles il n'est dit « ça doit être proportionné à la taille de la tâche ».
Autrement dit : en optimisant contre l'échec « déclarer validé trop facilement » de HT001, on a fabriqué l'échec exactement inverse — le sur-contrôle. Celui-là est de notre fait, pas de celui du modèle.
Cause racine n° 3 : cumul avec l'issue #3¶
Ce qui bloquait réellement, ce sont deux défauts de portabilité sur macOS :
src/proc.cpputilise::sigemptyset, or le SDK Apple le redéfinit après le prototype de fonction sous forme de macro fonctionnelle (gardée uniquement par#ifndef _ANSI_SOURCE), donc::sigemptyset(&s)s'expanse en::(*(&s)=0,0), erreur de syntaxesrc/ui.cpputilise::_exitsans inclure<unistd.h>
Trois lignes suffisaient. Mais l'utilisateur avait répondu « ne pas toucher au code métier » en phase de clarification, l'agent a appliqué strictement, et il est parti tester des flags de compilation, dépêcher des subagents, écrire des journaux de décision. S'il avait pu glisser à ce moment-là « les deux lignes, tu peux les changer », c'était fini en dix minutes. Une limite écrite comme une interdiction sera interprétée aussi strictement que possible, et il n'existe aucun canal en cours de route pour la relâcher.
L'enregistrement brut montre la longueur du détour : quatre points de blocage au total, dont un seul — celui de ::_exit — pouvait se résoudre par des flags de compilation ; celui de ::sigemptyset a essuyé 6 combinaisons de -D/-U, toutes en échec (la famille _POSIX_C_SOURCE casse en prime st_mtimespec et O_CLOEXEC). Cette impasse a à elle seule produit un rapport de faisabilité de 13.6K.
Mais rendons justice : la limite a forcé une meilleure solution¶
La solution finale n'est pas de modifier le source, mais de générer dans le Makefile un header cale qui ne fait que des #undef, puis de le forcer en tête avec -include :
$(MACSHIM):
@printf '#include <signal.h>\n#undef sigemptyset\n#undef sigfillset\n...' > $@
CPPFLAGS += -include $(MACSHIM) -include unistd.h
C'est mieux que de modifier le source — le dépôt amont ne bouge pas d'une ligne, n'importe qui peut cloner et compiler. La cale est générée dans build/, non versionnée, clean récupère avec rm -rf build, et un make -n UNAME_S=Linux réel montre qu'après expansion il n'y a pas de -include : le chemin Linux n'est pas pollué. Au final, le git diff ne porte que sur un seul fichier, Makefile, +14 / −2.
Donc la limite a produit une meilleure solution à un coût plus élevé. Le problème n'est pas qu'elle ait mal fait, c'est que cet arbitrage n'a pas été confié à l'humain — et la raison en est précisément l'issue #3.
Pourquoi git status affiche 64 entrées alors que le source n'a pas bougé
Le dépôt cppide a committé dans le versionnement 30 .o, 30 .d et 2 binaires, en ELF Linux aarch64, avec des mtime postérieures aux .cpp — sans un make clean préalable, make saute la compilation et va linker directement les ELF, échec garanti. Donc sur les 64 entrées de git status --porcelain, 63 sont des artefacts de compilation suivis par git, plus un ?? build/. Après filtrage par extension, git diff -- 'src/*.cpp' 'src/*.h' README.md config.sample.json | wc -c donne 0. Cette formulation précise a été arrachée par l'audit de conformité — le coordinateur voulait initialement écrire vaguement « zéro modification dans src/ », l'audit a montré que la phrase ne tenait pas.
2. Ce que le goal guard a fait correctement¶
Tout le mécanisme a fonctionné, et les deux modifications du jour ont laissé une empreinte directe dans les livrables.
Le premier critère de sa checklist de verdict :
uname -aindique Darwin 25.2.0 (macOS)… tous les critères « tourne / s'ouvre » ci-dessous doivent être des résultats réellement obtenus sur cette machine, on n'accepte pas les déductions du type « le source contient une branche macOS donc ça devrait tourner »
C'est exactement la phrase écrite ce matin-là dans JUDGE_RULES à partir des leçons de HT001.
Il a utilisé trois fois le marqueur [此环境无法验证:…] ajouté le jour même, avec à chaque fois une raison — console devtools (aucun navigateur pilotable, et installer un navigateur headless pour ça franchirait la limite), clic manuel (nécessite un humain devant l'écran), confirmation par l'intéressé (lui seul peut la donner).
La première phrase du juge est « je ne conclus pas à partir de ce compte rendu. Je vais sur place. » Puis :
file cppide → Mach-O 64-bit executable arm64
lsof -p 96040 → txt pointe vers /Users/hechenyu/explore/test-ide/cppide/cppide
démarré à 16:10, toujours vivant à 16:15
Il écrit lui-même : « le piège du "livrer un ELF Linux" des leçons méta, cette fois on ne l'a pas pris. Un ELF Linux ne peut pas donner un PID vivant sur Darwin. Ce n'est pas déduit du Makefile. »
Il a aussi vérifié que le source n'avait effectivement pas bougé : toutes les mtime des .cpp / .h sous src/ sont à 15:32 (l'instant du clone), seul le Makefile a changé (16:06), et les .o / .d (16:07) sont des artefacts de compilation suivis, reconstruits.
Il a aussi avoué honnêtement ce qu'il ne pouvait pas atteindre. Le Bash du juge est restreint au répertoire de travail : la comparaison via git ls-remote et la mtime de ~/.zshrc, il ne pouvait pas les exécuter, seulement s'en remettre à la sortie de l'audit indépendant — il a listé ces deux points à part en précisant « je n'ai pas pu les rejouer moi-même ».
3. Verdict « inatteignable », puis on demande à l'humain¶
Verdict à trois états plus intervention humaine : toute la chaîne a fonctionné pour la première fois en conditions réelles.
Le verdict est inatteignable, bloqué sur le critère de capture d'écran : screencapture -x et -l <window-id> renvoient tous deux could not create image from display — l'autorisation TCC d'enregistrement de l'écran n'est pas accordée, et l'accorder demande à un humain de cliquer dans les réglages système, ce qui est justement une modification système interdite par les limites.
« Dépêcher un tour de subagents de plus ne fera pas apparaître cette image. Selon les règles, on n'a pas le droit de valider, et ce n'est pas non plus un "pas encore atteint" : c'est bel et bien inatteignable, il faut s'arrêter et rendre la main à l'humain. »
Les critères « invérifiables par principe » passent donc de 3 à 4 — 4 des 15 critères sont tout simplement invérifiables dans cet environnement, et on ne s'en aperçoit qu'après avoir dépensé $35.8950 de travail et $1.4037 de verdict. L'étape de définition de l'objectif n'avait coûté que $0.4117 : c'est là qu'on aurait dû le voir.
Le framework a posé cette conclusion à l'humain (consigné en q7 du journal de questions-réponses), trois options, l'humain a choisi « accepter ce résultat », le gate a laissé passer, le workflow s'est terminé.
Il a aussi refusé un substitut. L'exécutant a relu le texte de la fenêtre de terminal via AppleScript, c'était bien une TUI réellement rendue — colonne de numéros de ligne, corps de src/main.cpp, barre de panneaux ─[编译]─[运行]─[AI*]─[输入]─, barre d'état 练习模式 │ main.cpp │ 1:1 │ C++, ni écran blanc ni écran d'erreur. Le juge dit :
« Celui qui a fait le travail n'a pas fait passer ça pour "avoir regardé l'image", cette retenue est correcte. Mais ça ne remplace pas le critère demandé par la checklist, et je ne vais pas non plus compter ça à sa place. »
« Avoir divulgué le risque » n'équivaut pas à valider
C'est exactement là que HT001 est tombé : il avait écrit « jamais exécuté une seule fois sur macOS » dans la note de livraison, puis avait quand même déclaré le critère 1 validé. Dans HT002, le juge disposait d'une preuve de substitution meilleure (le texte réel de la TUI) et a tout de même refusé de l'utiliser à la place du critère. Cette différence, c'est tout le sens du verdict à trois états — UNREACHABLE n'est pas une façon polie de dire NOT_YET, c'est « il faut s'arrêter et demander à l'humain ».
4. allowed_tools n'est pas une whitelist stricte¶
En analysant ce run, un fait contredisant ce qui était affirmé est apparu. Appels d'outils décomposés par session :
| Session | Réellement utilisé | Contenu de la whitelist |
|---|---|---|
| Clarifier les besoins | WebFetch, Glob, ask×6 | clarify() ne donnait alors que ask + Read/Glob/Grep |
| Fixer l'objectif | Bash ×11 | judge() a can_run=False par défaut, pas de Bash |
| Travail · verdict | Bash ×31, WebFetch | idem ci-dessus |
| Travail (coordinateur) | Bash ×1, Agent ×6 | correct |
goal_step n'a aucun chemin de code qui passe can_run, donc la ligne « Fixer l'objectif » ne peut pas être un problème de configuration.
Vérification par sonde à $0.1 : donner à un agent avec allowed_tools=["Read"] l'ordre d'écrire un fichier —
Write → couche de permissions : "requested permissions to write ... but you haven't granted it yet"
Bash → sécurité des chemins : "Output redirection was blocked. For security, Claude Code may
only write to files in the allowed working directories"
Le modèle peut appeler des outils absents de la whitelist, il est simplement arrêté par la couche de permissions et la sécurité des chemins. Donc :
allowed_toolsest une liste de dispense d'approbation, pas une whitelist exclusive- ce qui protégeait réellement
clarify()/judge()à l'époque, c'était lepermission_mode="default"hérité - et tous deux avaient
delegate_only=False→ pas de hookdelegate_guard. Le coordinateur était protégé par un hook, ces deux rôles n'avaient alors aucun mécanisme propre à flower
La docstring de clarify() disait « pas d'outils d'écriture — il ne peut pas se mettre au travail », et tout le contre-test réel à $0.8908 avait été fait pour cette phrase. La justification mécanique de cette phrase est fausse.
Les deux faits de cette section ont changé depuis
whitelist_guard est désormais branché sans condition : les rôles avec delegate_only=False reçoivent automatiquement ce hook du Runtime, clarify() / judge() ne sont plus « sans aucun mécanisme ». Par ailleurs WebFetch est maintenant dans la whitelist de clarify(), donc l'exemple de la première ligne du tableau est caduc — la conclusion tient toujours, mais elle ne repose plus que sur la preuve de la sonde à $0.1. Voir la dernière section de cette page.
5. La courbe de contexte : les frontières d'étape la réinitialisent¶
Tour 1 36.8K
Tour 25 80.2K ← pic
Tour 26 32.9K ← chute : nouvelle étape = nouvelle session (resume_from=None)
Tour 121 64.8K
Trois des quatre points d'échantillonnage (tours 1 / 25 / 121, soit 36.8K / 80.2K / 64.8K) correspondent un à un à l'enregistrement brut ; le 32.9K du tour 26 ne retombe pas juste au recalcul : en rejouant les messages assistant du thread principal triés par store_key, le 26e (le premier de la nouvelle session) donne input + cache_read + cache_creation = 31,972 tokens = 32.0K, tandis que 32.9K est la deuxième valeur de la même session (31,970 + 948). Cette page conserve 32.9K — l'écart est de 0.9K, et la conclusion « chute brutale » tient avec l'une comme avec l'autre.
HT001 montait de façon monotone jusqu'à 185.9K — une seule étape qui a tourné 10 heures. HT002, découpé en quatre étapes ouvrant chacune une nouvelle session, voit son contexte réinitialisé structurellement. C'est l'effet direct du principe « le brief et l'objectif sont des pièces gelées, l'aval reçoit les documents et non la conversation » — ça se voit sur la courbe.
Hits de cache : 90.8% (HT001 : 96.1%). Le taux de hits croît avec la longueur de session, donc les sessions courtes coûtent un peu plus cher à l'unité — c'est l'autre face de la même pièce que la conclusion de la troisième section de HT001.
6. Comparaison avec HT001¶
| HT001 | HT002 | |
|---|---|---|
| Tâche | écrire un IDE terminal à partir de zéro | l'installer sur macOS et le faire tourner |
| Coût / durée | $171.62 / 10.44h | $38.24 / 0.97h |
| Étapes | 2 (sans goal guard) | 4 (dont définition d'objectif + verdict) |
| subagents | 23 | 6 |
| Appels d'outils du coordinateur | 32 | 7 (dont 6 Agent) |
| Appels d'outils des subagents | 1,893 | 212 |
| Contexte du thread principal | 28.7K → 185.9K, croissance monotone | 36.8K → 80.2K, réinitialisé aux frontières d'étape |
| Hits de cache | 96.1% | 90.8% |
| Scripts du workbench | 58, exécutés 331 fois, 92% réutilisés | 0 |
| Verdict | aucun (auto-audit spontané du coordinateur) | verdict à trois états, « inatteignable », remonté à l'humain |
La dernière ligne mérite l'attention. HT002 n'a écrit aucun script réutilisable — la tâche ne durait qu'une heure, il n'y avait rien à sédimenter. Cela montre que la valeur du workbench croît avec la longueur de la tâche ; sur une tâche courte, c'est du surcoût pur.
À quoi ressemble l'enregistrement brut¶
.flower/INDEX.md est l'instantané du workbench à la fin de ce run, on peut le lire directement :
| Répertoire | Contenu |
|---|---|
scripts/ | 0 |
artifacts/ | 5 — 01-recon.md (32.6K), 02-build-run.md (19.4K), 03-flag-only-feasibility.md (13.6K), 04-compliance-audit.md (16.4K), 05-final-build-run.md (19.8K) |
notes/ | 4 — 需求.md, 目标.md, 问答记录.md sont trois pièces gelées écrites par le framework, l'agent n'a écrit qu'un seul journal de décision |
spill/ | 11 fichiers de spill |
Sous runs/ se trouvent manifest.json (4 enregistrements d'étape) et sessions.db. La base contient 10 sessions : 4 sessions de thread principal plus 6 sessions de subagents — cohérent avec les « 6 subagents ».
Le journal de questions-réponses compte 7 questions au total : q1–q6 posées en phase de clarification préalable (cohérent avec les 6 appels ask de l'étape de clarification des besoins), q7 étant le gate humain après le verdict « inatteignable ».
Ce que ce run a changé dans le framework¶
| Constat | Changement effectif |
|---|---|
| Sur 15 critères de verdict, environ 5 seulement vérifient si ça marche (section 1) | ajout d'une pression inverse dans JUDGE_RULES : la longueur de la checklist est déterminée par le nombre de modes d'échec, pas par le degré de rigueur, et ce run a été inscrit comme contre-exemple dans le texte des règles (« une tâche "installer et faire tourner" écrite en 15 critères, dont 5 seulement vérifient si ça marche » — la règle arrondit à 5, la classification de la section 1 de cette page dit environ 5) |
| Les limites écrites comme critères de verdict | JUDGE_RULES fige la phrase « une limite n'est pas un critère de verdict » |
| 4/15 critères invérifiables dans cet environnement, découvert seulement après $35.8950 + $1.4037 dépensés | l'étape de définition de l'objectif doit désormais suffixer sur-le-champ les critères invérifiables par [此环境无法验证:原因] — cette fois il l'a bien fait pour 3 critères, et a manqué celui de la capture d'écran |
clarify() / judge() sans protection par hook (section 4) | whitelist_guard est branché sans condition : les rôles avec delegate_only=False le reçoivent automatiquement du Runtime, plus de dépendance à l'activation du workbench |
| Effet de bord : le juge de la définition d'objectif n'a plus accès à Bash | avec whitelist_guard, si can_run=True n'est pas passé explicitement, la règle « commence par un uname -a pour voir où tu es » de JUDGE_RULES est inexécutable — c'est un arbitrage nouveau, apparu après le correctif |
La justification mécanique de la docstring de clarify() était fausse | corrigé. WebFetch est aussi entré dans la whitelist de clarify(), donc cet exemple est caduc, la conclusion repose désormais sur la sonde à $0.1 |
| Aucun canal pour glisser un mot en cours de route (cause racine n° 3 de la section 1) | consigné en issue #3, non résolu — cette fois elle s'est cumulée directement avec la sur-complexification, un simple « les deux lignes, tu peux les changer » aurait économisé une heure |
Ce run en une phrase : optimiser contre l'échec précédent produit très facilement un échec neuf, orienté dans l'autre sens. HT001 a appris au framework « ne valide pas à la légère », HT002 a immédiatement démontré ce qui se passe quand cette règle n'est pas accompagnée d'un « ne sur-contrôle pas ». Les deux doivent figurer dans JUDGE_RULES ; s'il en manque une, ça penche.