HT002 · Install it on macOS and get it running¶
2026-09-07. The task was one sentence: clone cppide, install it on this macOS machine, get it running. The result was $38.24 / 4 steps / about 1 hour. The program did run on macOS (PID 96040, arm64 Mach-O), and the verdict was unreachable, surfaced to the human, who chose to accept it.
This was the first run with a goal guard —— everything built out of the HT001 lessons, all of it hitting a real API for the first time that day. It saved us from one pit HT001 fell into, and at the same time produced a new failure pointing in exactly the opposite direction: it turned git clone && make && ./cppide into an hour. The most valuable part of this page is the latter.
The raw record is in human-test/HT002/, and the accounting is computed from runs/manifest.json and runs/sessions.db:
The shape of this run¶
| Item | Value |
|---|---|
| Task | clone cppide, install it on this macOS machine, get it running, human can actually open the UI and use it |
| Cost | $38.24 —— 4 steps, breakdown below |
| Duration | about 1 hour (0.97h total) |
| Turns | 70 (sum of num_turns over the four steps) |
| Main thread context | 36.8K → 80.2K, drops off a cliff at step boundaries, not monotonic growth |
| Division of labor | 6 subagents; coordinator 7 tool calls vs subagents 212 |
| Cache | 90.8% hit rate |
| Workbench | 5 artifacts, 4 notes, 11 spill files, 0 reusable scripts |
| Outcome | it ran (PID 96040, arm64 Mach-O); verdict unreachable, human chose to accept |
| Version | first run with a goal guard |
Cost, turns and duration per step¶
| Step | Cost | Turns (num_turns) | Duration |
|---|---|---|---|
| Clarify requirements | $0.5306 | 9 | 0.10h |
| Set goal | $0.4117 | 12 | 0.05h |
| Work | $35.8950 | 12 | 0.73h |
| Work · verdict #1 | $1.4037 | 37 | 0.09h |
| Total | $38.2409 | 70 | 0.97h |
The duration column is the sum of the per-step values after rounding. Recomputed from the raw record, the four duration_s values in manifest.json sum to exactly 3471.53 seconds = 0.9643h; this page keeps 0.97h, a difference of 0.006h.
Almost all the money is in the "work" step —— $35.8950, 93.9% of the total. The first section below explains where that money went, and why it should not have gone there.
1. Headline: it turned git clone && make && ./cppide into an hour¶
This is the most important finding of the run, more important than every "the mechanism works" below.
In the set-goal step, the judge wrote itself 15 verdict checklist items (the original is in 目标.md under .flower/notes/). By nature:
| Category | Count | Example |
|---|---|---|
| Actually verifies "the thing works" | about 5 | build exit 0; process alive ≥30 seconds; restart command re-runnable in a fresh shell |
| Verifies "the process behaved" | 6 | mtimes of ~/.zshrc / .zprofile / .bash_profile all earlier than this session; no brew install / npm -g / sudo was run; .flower/ mtime unchanged, nothing added to or removed from runs/ |
| Unverifiable in principle | 4 | take a screenshot and look at it; devtools console; manual clicking; the person confirms in person |
Only a third are verifying whether the thing actually works. Six are verifying whether it followed the rules itself —— one of which checks the mtime of the .flower/ directory, which is the framework's own directory.
The item checking .flower/ failed itself¶
This is not a theoretical problem. In the verdict round, the judge wrote honestly under "not passed":
All files under
.flower/have mtimes earlier than session start —— literal failure:INDEX.md16:14,artifacts/01–05(15:40–16:12),notes/决策-项目形态与验收路径.md15:43 were all written during the session.
The cause: the framework hard-requires long outputs to be written into .flower/artifacts/ and decisions into .flower/notes/. That checklist item requires the directory not to change. Two rules collide head-on, and the collision was manufactured by the checklist itself —— the worker didn't write a single long artifact, which made it more compliant.
It didn't paper over it: the work step listed this conflict explicitly in its final report, and the decision record wrote down how it was handled —— not a single existing file was touched (需求.md, 目标.md, 问答记录.md all have identical mtimes and MD5s before and after), only additions. The judge reviewed and agreed that "the tampering this item guards against did not happen", but still listed it as not passed per the rules and handed it to the human.
Root cause 1: boundaries were turned into verdict items¶
The boundaries elicited in the clarify phase ("install only inside the project directory", "don't touch business code") were meant to constrain how the work is done, and instead got written as checks verifying the output. That's where "check .zshrc mtime", "check .flower/ mtime" come from.
Boundaries ≠ verdict items. Boundaries constrain the process; verdict items verify the result. Once conflated, every extra boundary becomes an extra verdict item —— and boundaries are exactly what the clarify phase encourages you to fill in, since CLARIFIER_RULES explicitly says "state what is not to be done. This section governs everyone who does the work afterwards."
Against the raw record you can see every link in this amplification chain: the brief's boundary says "don't touch the existing .flower/ and runs/ in the current directory", and by the time it reached the verdict checklist it had escalated to "all files under .flower/ have mtimes earlier than session start" —— from "don't touch what's there" to "no new files anywhere in the directory".
Root cause 2: JUDGE_RULES only pushes in one direction —— and it had been added that day¶
Earlier that day, out of the HT001 lessons, these went into JUDGE_RULES:
- "every item must be verifiable on the spot", "anything missing or vague, you fill it in"
- "you judge the artifact, not the source" (with that macOS counter-example)
- "items that cannot be verified in the current environment, mark them on the spot"
- "first figure out where you are (you may run
uname -a)"
Every one of them pushes toward more checks, stricter checks. Nowhere in the whole rule set is there a sentence saying "keep it proportional to the size of the task".
In other words: optimizing against HT001's "over-claiming pass" failure produced the exact opposite failure —— over-checking. This one is self-inflicted, not a model problem.
Root cause 3: it compounded with issue #3¶
What actually blocked things were two portability defects on macOS:
src/proc.cppuses::sigemptyset, but the Apple SDK defines it as a function-like macro after the prototype (guarded only by#ifndef _ANSI_SOURCE), so::sigemptyset(&s)expands to::(*(&s)=0,0), a syntax errorsrc/ui.cppuses::_exitwithout including<unistd.h>
Three lines would have fixed it. But the user had answered "don't touch business code" in the clarify phase, and the agent enforced it strictly, so it detoured into trying compile flags, dispatching subagents, writing decision records. If he had been able to drop in one line —— "changing those two lines is fine" —— it would have been over in ten minutes. A boundary written as a prohibition gets interpreted as strictly as possible, and there was no channel mid-run to loosen it.
The raw record shows how long the detour was: four blocking points in total, of which only the ::_exit one could be solved by pure compile flags; the ::sigemptyset one had 6 combinations of -D/-U tried and all failed (the _POSIX_C_SOURCE family also breaks st_mtimespec and O_CLOEXEC along the way). This dead end alone produced a 13.6K feasibility report.
But to be fair: the boundary forced out a better solution¶
The final approach was not to edit the source, but to generate a shim header that only does #undef in the Makefile and force it in front with -include:
$(MACSHIM):
@printf '#include <signal.h>\n#undef sigemptyset\n#undef sigfillset\n...' > $@
CPPFLAGS += -include $(MACSHIM) -include unistd.h
This is better than editing the source —— the upstream repo needs zero changes, and anyone who clones it can build. The shim is generated into build/, not committed, clean gained rm -rf build to reclaim it, and make -n UNAME_S=Linux was measured to expand without any -include, so the Linux path is uncontaminated. The final git diff touches only Makefile, +14 / −2.
So the boundary produced a better solution at a higher cost. The problem isn't that it did the wrong thing, it's that this trade-off was never put to the human —— and the reason it couldn't be is exactly issue #3.
Why git status shows 64 entries yet the source is unchanged
The cppide repo commits 30 .o files, 30 .d files and 2 binaries into version control, and they're Linux ELF aarch64 with mtimes later than the .cpp files —— without a make clean first, make skips compiling and links the ELF objects directly, which must fail. So of the 64 entries in git status --porcelain, 63 are git-tracked build products plus one ?? build/. Filtered by extension, git diff -- 'src/*.cpp' 'src/*.h' README.md config.sample.json | wc -c is 0. This precise phrasing was forced out by the compliance audit —— the coordinator originally wanted to write a loose "src/ unchanged", and the audit pointed out that statement doesn't hold.
2. What the goal guard got right¶
Everything on the mechanism side worked, and the two changes made that day left direct fingerprints in the output.
The first item of the verdict checklist it wrote:
uname -ashows Darwin 25.2.0 (macOS)…… every "runs / opens" item below must be a result actually obtained by running on this machine, inferences like "the source has a macOS branch so it should run" are not accepted
That is exactly the sentence written into JUDGE_RULES that morning from the HT001 lessons.
It used the [cannot be verified in this environment: …] marker added that day three times, each with a reason —— devtools console (no drivable browser, and installing a headless one would cross the boundary), manual clicking (needs a human in front of the screen), personal confirmation (only he can give it).
The judge's first sentence was "I'm not drawing conclusions from that report. Go to the scene." Then:
file cppide → Mach-O 64-bit executable arm64
lsof -p 96040 → txt points at /Users/hechenyu/explore/test-ide/cppide/cppide
started 16:10, still alive at 16:15
It wrote itself: "the 'delivered a Linux ELF' pit from the meta lessons —— we didn't step in it this time. A Linux ELF cannot become a PID on Darwin. This is not inferred from a Makefile."
It also checked that the source really was untouched: every .cpp / .h under src/ has mtime 15:32 (the moment of clone), the only change is Makefile (16:06), and the .o / .d files (16:07) are tracked build products being rebuilt.
It also stated honestly what it couldn't reach. The judge's Bash was confined to the working directory, so it couldn't run the git ls-remote comparison or the ~/.zshrc mtime check and had to take the independent audit's output on trust —— it listed those two separately to say "I couldn't re-run these myself".
3. Verdict "unreachable", then ask the human¶
Three-state verdict plus human intervention: the whole chain ran for real for the first time.
The verdict was unreachable, stuck on the screenshot item: both screencapture -x and -l <window-id> returned could not create image from display —— the screen recording TCC permission wasn't granted, and granting it requires a human to click through System Settings, which is itself a system change forbidden by the boundaries.
"Dispatching another round of subagents won't conjure this image. By the rules this can't be judged pass, and it shouldn't be judged 'not yet' either. It is unreachable, and the right move is to stop and hand it to the human."
So the "unverifiable in principle" items went from 3 to 4 —— 4 of the 15 items simply cannot be verified in this environment, and this was discovered after spending $35.8950 on work and $1.4037 on the verdict. The set-goal step only cost $0.4117; that's where it should have been found.
The framework put this conclusion to the human (recorded in the Q&A record as q7), three options, the human picked "accept this result", the gate opened, the workflow completed.
It also rejected a substitute. The worker used AppleScript to read back the terminal window text, and it was a genuinely rendered TUI —— line-number gutter, the body of src/main.cpp, the panel bar ─[编译]─[运行]─[AI*]─[输入]─, the status bar 练习模式 │ main.cpp │ 1:1 │ C++ —— not a blank screen, not an error screen. The judge said:
"The worker didn't pass this off as 'looking at the image', and that self-restraint is right. But it doesn't substitute for the item the checklist asks for, and I'm not going to count it for him either."
"Disclosing the risk" is not the same as passing
This is exactly what HT001 fell into: it wrote "never once run on macOS" into the delivery notes, and still judged acceptance criterion 1 as passed. In HT002 the judge got a better piece of substitute evidence (real TUI text) and still refused to let it stand in for the checklist item. That distinction is the entire point of the three-state verdict design —— UNREACHABLE is not a polite way of saying NOT_YET, it means "stop here and ask the human".
4. allowed_tools is not a hard whitelist¶
Analyzing this run turned up a fact that contradicts the claim. Broken down by session, the tool calls were:
| Session | Actually used | What's in the whitelist |
|---|---|---|
| Clarify requirements | WebFetch, Glob, ask×6 | clarify() only granted ask + Read/Glob/Grep at the time |
| Set goal | Bash ×11 | judge() defaults to can_run=False, no Bash |
| Work · verdict | Bash ×31, WebFetch | same as above |
| Work (coordinator) | Bash ×1, Agent ×6 | correct |
goal_step has no code path that passes can_run at all, so the "set goal" row cannot be a configuration issue.
A $0.1 probe confirmed it: give an agent with allowed_tools=["Read"] a file to write ——
Write → permission layer: "requested permissions to write ... but you haven't granted it yet"
Bash → path safety: "Output redirection was blocked. For security, Claude Code may
only write to files in the allowed working directories"
The model can invoke tools that aren't in the whitelist, it just gets stopped by the permission layer and path safety. So:
allowed_toolsis a pre-approval list, not an exclusive whitelist- what actually protected
clarify()/judge()at the time was the inheritedpermission_mode="default" - and both had
delegate_only=False→ nodelegate_guardhook. The coordinator has a hook guarding it; these two roles had no flower mechanism of their own at all
clarify()'s docstring says "no write tools —— it can't start doing the work", and that $0.8908 counter-experiment existed entirely to back that sentence. The mechanism this sentence rests on was wrong.
Both facts in this section have since changed
whitelist_guard is now wired in unconditionally: roles with delegate_only=False get the hook installed automatically by Runtime, so clarify() / judge() are no longer "without any mechanism". Also WebFetch is now in clarify()'s whitelist, so the example in the first table row is void —— the conclusion still holds, but it now rests solely on the $0.1 probe. See the last section of this page.
5. The context curve: step boundaries reset it¶
turn 1 36.8K
turn 25 80.2K ← peak
turn 26 32.9K ← cliff: new step = new session (resume_from=None)
turn 121 64.8K
Three of the four sample points (turns 1 / 25 / 121, 36.8K / 80.2K / 64.8K) match the raw record exactly; the 32.9K at turn 26 doesn't reconcile: replaying main-thread assistant messages sorted by store_key, the 26th (the first of the new session) has input + cache_read + cache_creation = 31,972 tokens = 32.0K, while 32.9K is the second value in the same session (31,970 + 948). This page keeps 32.9K —— the gap is 0.9K, and the "cliff drop" conclusion holds under either value.
HT001 climbed monotonically to 185.9K —— a single step that ran for 10 hours. HT002, split into four steps each opening a new session, has its context structurally reset. That's the direct effect of the design where the brief and the goal are frozen artifacts and downstream takes the documents, not the conversation —— you can see it in the curve.
Cache hit rate 90.8% (HT001 was 96.1%). Hit rate rises with session length, so short sessions are actually a bit more expensive per unit —— this is the other side of the same coin as HT001's third section.
6. Compared with HT001¶
| HT001 | HT002 | |
|---|---|---|
| Task | write a terminal IDE from scratch | install it on macOS and get it running |
| Cost / duration | $171.62 / 10.44h | $38.24 / 0.97h |
| Steps | 2 (no goal guard) | 4 (incl. set goal + verdict) |
| Subagents | 23 | 6 |
| Coordinator tool calls | 32 | 7 (6 of them Agent) |
| Subagent tool calls | 1,893 | 212 |
| Main thread context | 28.7K → 185.9K monotonic | 36.8K → 80.2K, reset at step boundaries |
| Cache hit rate | 96.1% | 90.8% |
| Workbench scripts | 58, executed 331 times, 92% reused | 0 |
| Verdict | none (coordinator self-reviewed on its own initiative) | three-state verdict, judged "unreachable", asked the human |
The last row deserves attention. HT002 wrote zero reusable scripts —— the task was one hour long, there was nothing worth accumulating. This says the workbench's value grows with task length; in a short task it is pure overhead.
What the raw record looks like¶
.flower/INDEX.md is the workbench snapshot at the end of the run, and you can read it directly:
| Directory | Contents |
|---|---|
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 are three frozen artifacts written by the framework; the agent wrote only 1 decision record itself |
spill/ | 11 spill files |
Under runs/ are manifest.json (4 step records) and sessions.db. The database holds 10 sessions: 4 main-thread sessions plus 6 subagent sessions —— consistent with "6 subagents".
The Q&A record has 7 questions in total: q1–q6 were asked in the clarify phase (consistent with the 6 ask calls in the clarify-requirements step), and q7 is the human gate after the "unreachable" verdict.
What this run changed in the framework¶
| Finding | Change that landed |
|---|---|
| Only about 5 of 15 verdict items verified whether it works (section 1) | JUDGE_RULES gained counter-pressure: checklist length is determined by the number of ways it can fail, not by how rigorous you feel, and this run was written into the rule text as a counter-example ("a 'get it installed and running' task written up as 15 items, only 5 of which verify whether it works" —— the rules round it to 5, this page's section 1 classifies it as about 5) |
| Boundaries got written as verdict items | JUDGE_RULES now states flatly: "boundaries are not verdict items" |
| 4/15 items unverifiable in this environment, discovered only after spending $35.8950 + $1.4037 | the set-goal step now requires marking unverifiable items on the spot with a [cannot be verified in this environment: reason] suffix —— it used this correctly on 3 items this time and missed the screenshot one |
clarify() / judge() had no hook protection (section 4) | whitelist_guard is now wired in unconditionally: roles with delegate_only=False get it installed automatically by Runtime, no longer dependent on whether the workbench is enabled |
| Side effect: the set-goal judge can no longer get Bash | with whitelist_guard in place, unless can_run=True is passed explicitly, the JUDGE_RULES line "first run uname -a to see where you are" cannot be executed —— a new trade-off created by the fix |
clarify()'s docstring rested on the wrong mechanism | fixed. WebFetch also entered clarify()'s whitelist, so that example is void and the conclusion now hangs on the $0.1 probe |
| No channel to drop in a sentence mid-run (section 1, root cause 3) | filed as issue #3, still open —— this time it compounded directly with the over-complication, and one line of "changing those two lines is fine" would have saved an hour |
One sentence to sum up this run: optimizing against the previous failure very easily produces a new failure pointing the opposite way. HT001 taught the framework "don't judge pass too easily"; HT002 immediately demonstrated what happens when that rule isn't paired with "don't over-check". Both have to go into JUDGE_RULES —— with either one missing, it skews.