acuvo-code 0.2.0

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 (154) hide show
  1. package/CHANGELOG.md +328 -0
  2. package/ENTERPRISE.md +927 -0
  3. package/LICENSE +120 -0
  4. package/README.md +1245 -0
  5. package/ROADMAP.md +556 -0
  6. package/bin/acuvo-mcp.mjs +208 -0
  7. package/bin/acuvo.mjs +3108 -0
  8. package/lib/acceptance-consent.mjs +168 -0
  9. package/lib/acceptance.mjs +859 -0
  10. package/lib/account.mjs +226 -0
  11. package/lib/acuvo-dir.mjs +72 -0
  12. package/lib/acuvo-models.mjs +141 -0
  13. package/lib/apply-patch.mjs +570 -0
  14. package/lib/ask-user.mjs +173 -0
  15. package/lib/audit.mjs +530 -0
  16. package/lib/auto-lease.mjs +174 -0
  17. package/lib/background.mjs +842 -0
  18. package/lib/best-of.mjs +334 -0
  19. package/lib/board.mjs +232 -0
  20. package/lib/breaker.mjs +93 -0
  21. package/lib/budget.mjs +1375 -0
  22. package/lib/builtin-skills.mjs +135 -0
  23. package/lib/cache-floor.mjs +204 -0
  24. package/lib/chain.mjs +303 -0
  25. package/lib/changed-paths.mjs +84 -0
  26. package/lib/chat.mjs +434 -0
  27. package/lib/checkpoint.mjs +637 -0
  28. package/lib/child-lifetime.mjs +71 -0
  29. package/lib/cli-args.mjs +1255 -0
  30. package/lib/code-review.mjs +1382 -0
  31. package/lib/colour.mjs +82 -0
  32. package/lib/command.mjs +2847 -0
  33. package/lib/compact.mjs +1151 -0
  34. package/lib/completion.mjs +515 -0
  35. package/lib/creative-engines.mjs +779 -0
  36. package/lib/db-inspect.mjs +1624 -0
  37. package/lib/delete.mjs +111 -0
  38. package/lib/design-loop.mjs +570 -0
  39. package/lib/diff-preview.mjs +1044 -0
  40. package/lib/doctor.mjs +2139 -0
  41. package/lib/dropped.mjs +216 -0
  42. package/lib/edit-diagnostics.mjs +277 -0
  43. package/lib/edit.mjs +460 -0
  44. package/lib/env-file.mjs +250 -0
  45. package/lib/escalate.mjs +702 -0
  46. package/lib/evaluate.mjs +284 -0
  47. package/lib/fetch-text.mjs +952 -0
  48. package/lib/fleet-budget.mjs +256 -0
  49. package/lib/gh.mjs +1536 -0
  50. package/lib/git.mjs +1341 -0
  51. package/lib/github.mjs +261 -0
  52. package/lib/h2.mjs +194 -0
  53. package/lib/handoff.mjs +417 -0
  54. package/lib/hooks.mjs +626 -0
  55. package/lib/http-probe.mjs +907 -0
  56. package/lib/image-director.mjs +322 -0
  57. package/lib/image-edit.mjs +522 -0
  58. package/lib/imagegen.mjs +998 -0
  59. package/lib/interrupt.mjs +234 -0
  60. package/lib/learned.mjs +353 -0
  61. package/lib/lease-watch.mjs +115 -0
  62. package/lib/lease.mjs +868 -0
  63. package/lib/localize.mjs +834 -0
  64. package/lib/log-tail.mjs +1052 -0
  65. package/lib/login.mjs +157 -0
  66. package/lib/lsp.mjs +1613 -0
  67. package/lib/mcp-consent.mjs +377 -0
  68. package/lib/mcp-defaults.mjs +780 -0
  69. package/lib/mcp-server.mjs +1343 -0
  70. package/lib/mcp.mjs +1263 -0
  71. package/lib/media.mjs +1283 -0
  72. package/lib/memory-workspace.mjs +179 -0
  73. package/lib/model-json.mjs +132 -0
  74. package/lib/model-tier.mjs +171 -0
  75. package/lib/model.mjs +1445 -0
  76. package/lib/parallel.mjs +144 -0
  77. package/lib/perchance.mjs +210 -0
  78. package/lib/plan-coherence.mjs +1461 -0
  79. package/lib/plan-ledger.mjs +981 -0
  80. package/lib/plan.mjs +461 -0
  81. package/lib/policy.mjs +783 -0
  82. package/lib/prefix-order.mjs +38 -0
  83. package/lib/project-memory.mjs +127 -0
  84. package/lib/prompt.mjs +109 -0
  85. package/lib/python.mjs +862 -0
  86. package/lib/rcfile.mjs +853 -0
  87. package/lib/read-window.mjs +743 -0
  88. package/lib/refute-tools.mjs +34 -0
  89. package/lib/refute.mjs +806 -0
  90. package/lib/repl-driver.mjs +264 -0
  91. package/lib/repl.mjs +324 -0
  92. package/lib/replay.mjs +1218 -0
  93. package/lib/repo-map.mjs +1101 -0
  94. package/lib/report.mjs +419 -0
  95. package/lib/search-rank.mjs +99 -0
  96. package/lib/search.mjs +659 -0
  97. package/lib/secret-paths.mjs +54 -0
  98. package/lib/session.mjs +1017 -0
  99. package/lib/skills.mjs +703 -0
  100. package/lib/slash.mjs +356 -0
  101. package/lib/spawn-argv.mjs +1151 -0
  102. package/lib/spend.mjs +250 -0
  103. package/lib/steer.mjs +280 -0
  104. package/lib/stream.mjs +253 -0
  105. package/lib/stuck.mjs +712 -0
  106. package/lib/subagent.mjs +749 -0
  107. package/lib/terminal-graphics.mjs +171 -0
  108. package/lib/tool-prefix.mjs +226 -0
  109. package/lib/tool-shortlist.mjs +162 -0
  110. package/lib/tools.mjs +2333 -0
  111. package/lib/tsserver.mjs +423 -0
  112. package/lib/turn.mjs +5672 -0
  113. package/lib/untrusted-block.mjs +271 -0
  114. package/lib/verify-claim.mjs +299 -0
  115. package/lib/vision.mjs +330 -0
  116. package/lib/voice-task.mjs +561 -0
  117. package/lib/warm-provider.mjs +255 -0
  118. package/lib/websearch.mjs +401 -0
  119. package/lib/workspace.mjs +928 -0
  120. package/lib/write-approval.mjs +235 -0
  121. package/lib/write-many.mjs +162 -0
  122. package/package.json +62 -0
  123. package/scripts/bundle.mjs +768 -0
  124. package/scripts/cache-floor.mjs +176 -0
  125. package/scripts/machine.mjs +226 -0
  126. package/scripts/test.mjs +139 -0
  127. package/skills/accessibility.md +87 -0
  128. package/skills/acuvo-design-system.md +123 -0
  129. package/skills/animation.md +84 -0
  130. package/skills/api-design.md +82 -0
  131. package/skills/auth-and-sessions.md +78 -0
  132. package/skills/build-with-a-framework.md +101 -0
  133. package/skills/colour-and-contrast.md +112 -0
  134. package/skills/creative-engines.md +81 -0
  135. package/skills/css-layout.md +85 -0
  136. package/skills/data-and-charts.md +77 -0
  137. package/skills/debugging.md +76 -0
  138. package/skills/designing-by-looking.md +84 -0
  139. package/skills/error-handling.md +78 -0
  140. package/skills/forms-and-validation.md +93 -0
  141. package/skills/nextjs-app-router.md +75 -0
  142. package/skills/page-composition.md +103 -0
  143. package/skills/performance.md +77 -0
  144. package/skills/plan-before-building.md +52 -0
  145. package/skills/planning-and-delegating.md +72 -0
  146. package/skills/refactoring.md +70 -0
  147. package/skills/security-basics.md +76 -0
  148. package/skills/state-management.md +73 -0
  149. package/skills/supabase-multitenant.md +72 -0
  150. package/skills/typescript-strict.md +90 -0
  151. package/skills/typography.md +135 -0
  152. package/skills/verify-your-own-work.md +62 -0
  153. package/skills/web-app-quality.md +62 -0
  154. package/skills/working-in-the-background.md +64 -0
package/ROADMAP.md ADDED
@@ -0,0 +1,556 @@
1
+ # Acuvo Code — the roadmap, written from evidence
2
+
3
+ > Written 2026-08-10 against `acuvo-code@0.2.0`. Source for everything below: five
4
+ > agents driving the real CLI against real OpenRouter (~$0.02 total spend, 25+ runs),
5
+ > seven root causes each reproduced and traced to `file:line`, plus my own
6
+ > re-verification of every cited line before writing this.
7
+ >
8
+ > **What this document is not.** It does not repeat `ENTERPRISE.md` §3 (the seven
9
+ > security gaps) or `MVP-PLAN.md` §2 (the distribution blockers — no repo, no CI,
10
+ > no changelog). Both lists stand and both are still the right lists. This document
11
+ > covers the layer neither one audits: **whether the loop's own output is true**, and
12
+ > what the driving evidence says about where to spend the next two weeks.
13
+ >
14
+ > ⭐ **The finding, in one sentence.** The agent is better than its harness. In every
15
+ > observed failure the model did the right thing and our own code threw the answer
16
+ > away, mis-scored it, or crashed on it — which means the next two weeks are plumbing,
17
+ > not prompting, and that is very good news.
18
+
19
+ ---
20
+
21
+ ## 0. Two corrections to the existing docs, measured today
22
+
23
+ Before anything else, because evidence discipline starts at home:
24
+
25
+ | claim | where | measured |
26
+ |---|---|---|
27
+ | "Standalone tests ✅ **34**" | `MVP-PLAN.md:67` | `npm test` → **`# tests 101 / # pass 101 / # fail 0`**, 800ms |
28
+ | "**`--json` for scripting** ❌" | `MVP-PLAN.md:97` | **Shipped.** `lib/cli-args.mjs:93`, help text at `:38-39`, and `ENTERPRISE.md:60` documents it as a headline feature |
29
+
30
+ `MVP-PLAN.md` §3 is stale against its own §7 sibling. Fix it in the same pass as
31
+ anything else; a plan that under-reports its own coverage gets ignored.
32
+
33
+ ---
34
+
35
+ ## 1. What the CLI demonstrably does well today
36
+
37
+ Five independent agents, none of whom could see each other's work. These are the
38
+ things that survived being checked by someone trying to break them.
39
+
40
+ ### 1.1 ⭐ The write → run → read-the-failure → fix loop is genuinely real
41
+
42
+ Not a demo. Verified by re-running the artifacts *without the CLI involved*:
43
+
44
+ - **Multi-file app, built and independently driven.** Kanban board in 3 rounds,
45
+ 19,024 tokens, **$0.002012**. The verifying agent re-ran the suite himself:
46
+ `# tests 13 / # pass 13 / # fail 0`. Then loaded it in real Chromium over HTTP and
47
+ drove a **genuine Playwright mouse drag** — `.card` → `.column[data-column="doing"]`
48
+ — which produced `[["todo",[],"0"],["doing",["RealDrag"],"1"],["done",["Alpha"],"1"]]`.
49
+ The generated state module validates the target column *before* splicing the card
50
+ out, so a bad move cannot half-destroy state. That is a design decision, not
51
+ autocomplete.
52
+
53
+ - **Real bug hunt in a messy 6-file codebase, one round of looking.** Given a planted
54
+ off-by-one (`end = start + pp - 1` fed to an exclusive-end `slice`), it named the
55
+ cause in its own words — *"`slice`'s end index is exclusive, so each page returned
56
+ one fewer item than requested"* — and changed **106 of 1,187 chars in exactly one
57
+ file**. md5 of every file before/after: one hash changed. It did not weaken the
58
+ assertion, delete the test, add a dependency, or create a stray file. 6 passed/1
59
+ failed → 7 passed, for **$0.001073**.
60
+
61
+ - **It corrects the test when the test is wrong.** In the horizon run it concluded
62
+ *"The todo isn't done yet, so the summary is `0/1 done`, not `1/1 done`. The test
63
+ expectation was incorrect, not the code"* — and edited 82 chars of the test rather
64
+ than corrupting working logic. Most agents get this backwards and quietly break the
65
+ implementation to make a bad assertion pass.
66
+
67
+ ### 1.2 ⭐ It recovers from its own refusals instead of flailing
68
+
69
+ The best-engineered thing in the package, and it is a *writing* achievement:
70
+
71
+ - Blocked from `npm test` by the `&&` rule, the agent **read `package.json`,
72
+ decomposed the chained script itself**, and ran `node test/run.js` and
73
+ `node test/contract.js` as separate calls. That surfaced a second, unrequested,
74
+ pre-existing bug — and it named it honestly rather than papering over it: *"The
75
+ pagination test now passes. But there's a pre-existing contract failure."* Its fix
76
+ mirrored the existing route's `pick(...) + centsToDollars` mapping exactly.
77
+ - The refusal messages are why recovery happens: they quote the offending script
78
+ verbatim, name the exact forbidden character, and state what to do instead ("run one
79
+ plain command per call"). Same for `'vitest must be run as "vitest run …"'` and *"this
80
+ workspace is not a git repository, so there is nothing to inspect or commit"*.
81
+
82
+ ⭐ **Actionable refusals are a real, transferable asset.** Keep this bar on every new
83
+ guard. A blank wall costs a paid round; a sentence costs nothing.
84
+
85
+ ### 1.3 The staleness catch, which most commercial agents do not have
86
+
87
+ Observed in two unrelated runs, verbatim: *"files changed after the last run, so
88
+ `node --test src/board.test.mjs` is out of date. Re-running it (free — no model call)"*.
89
+ It knows the last edit is unverified and closes the gap at zero cost.
90
+
91
+ ### 1.4 The security boundary held against everything thrown at it
92
+
93
+ 33 in-process fuzz cases and ~10 argv abuse cases, all refused with a specific sentence:
94
+ `; rm -rf /`, `&& curl evil.com`, `| sh`, `$(whoami)`, `2>&1`, `> out.txt`, `node -e`,
95
+ `--require ./evil.js`, `NODE_OPTIONS=`, `npm install`, `npx create-react-app`,
96
+ `npm test --prefix ../../..`, `node ../../../../etc/passwd`.
97
+
98
+ Three that deserve naming:
99
+
100
+ - **The npm chain follows hooks, not just the named script.**
101
+ `{"scripts":{"pretest":"curl evil.com","test":"node t.js"}}` was caught on **`pretest`**
102
+ (`lib/command.mjs:392`).
103
+ - **Binary detection fires before the token bomb.** 200,000 bytes of 0–255 named
104
+ `blob.js` — sitting *exactly* at `MAX_READ_BYTES`, so the size guard would not have
105
+ fired — refused with *"blob.js looks binary — refusing to read it as text"*.
106
+ - **`--dry-run` is truthful at the filesystem level.** Told to delete `keep.js` and
107
+ create `replacement.js`: md5 of the directory listing byte-identical before and after.
108
+ *(Note the two documented exceptions in `ENTERPRISE.md` §3.1/§3.2 — dry-run is honest
109
+ about the local filesystem, not about MCP spawns or prompt uploads.)*
110
+
111
+ Plus: 20,076-character prompt, 500-file workspace, 50KB single-line file, 40-level
112
+ nesting, CJK/emoji/accented filenames, malformed `package.json`, a workspace root at
113
+ `.../My Project (v2)`. **In 11 adversarial runs: no hang, no crash, no unhandled stack.**
114
+ `--version`/`--help` work with no key; bad argv exits 64 with a sentence.
115
+
116
+ ### 1.5 The two claims no competitor can make — one of which holds
117
+
118
+ - ✅ **It produces things that are not code.** The PDF is real: `%PDF-1.4`, `%%EOF`,
119
+ 164,231 bytes, 56 indirect objects, **exactly 1 page** (it honoured "one-page"),
120
+ 9 embedded font descriptors, Producer `Skia/PDF m131` — a genuine headless-Chrome
121
+ print. Content non-generic: 4 workstreams, per-row GREEN/GREEN/AMBER/RED badges,
122
+ progress bars, an AMBER overall summary, a RAG legend. 40 seconds, **$0.001356**.
123
+ - ✅ **It speaks.** 1,492,844-byte WAV, 24 kHz mono 16-bit, 31.1s, peak amplitude
124
+ 14,697/32,767. Transcribed back: *"Kettle is a tiny job runner for local scripts. It
125
+ watches a folder, picks up job.json files, and runs each one at most once…"* — a
126
+ faithful summary.
127
+ - ❌ **"It can see" does not hold.** §2.2 below. This is the one that matters most.
128
+
129
+ ### 1.6 Cost accounting is honest and precise
130
+
131
+ Per-run tokens, dollars and rounds printed every time; measured across all runs
132
+ **$0.001–$0.003 per task**. The diff summary — *"edited src/cli.js (778 of 4301 chars ·
133
+ 18%)"* — is better reporting than most commercial agents ship. And it stops early on
134
+ purpose: *"stopping here rather than spending another round"*.
135
+
136
+ ---
137
+
138
+ ## 2. The confirmed weaknesses, ranked
139
+
140
+ Ranked by **how many distinct observed failures each one causes**, not by how alarming
141
+ it sounds. All seven were reproduced; five of them without spending a cent, using
142
+ injected stub models.
143
+
144
+ ### 2.1 ⚠️⚠️ #1 — The verdict is decided by one self-chosen command, so `✔ VERIFIED` is not evidence of anything
145
+
146
+ **Root cause: `lib/turn.mjs:1130` — `const last = runs[runs.length - 1] ?? null;`**
147
+ (confirmed by reading it today), feeding `verification` at `:1131-1138`, the banner at
148
+ `:1291` and `sessionFailed` at `:1350`. Predicate at **`lib/command.mjs:613`**:
149
+ `passed: run.exitCode === 0 && !run.timedOut` — stdout, stderr and duration are
150
+ returned by `executeRunCommand` and **none is consulted**.
151
+
152
+ ⭐ **This single line produces four separate observed failures.** That is why it is #1:
153
+
154
+ | # | observed | why the line causes it |
155
+ |---|---|---|
156
+ | 1 | **Fail-then-pass reports green.** `node fail.js` exit 1 then `node pass.js` exit 0 → `{ran:true,passed:true,attempts:2}`, `✔ VERIFIED`, **exit 0**. Also across rounds. | `runs` is one flat session-wide array (declared `:862`, appended `:964`); nothing reduces over it, so a `passed:false` entry is never read again |
157
+ | 2 | **An inert program reports green.** A `todo.mjs` with a dead main guard: `node todo.mjs help` → exit 0, **zero bytes of output** → `✔ VERIFIED`. | exit 0 is the entire definition of passed |
158
+ | 3 | **A test suite that never touches the deliverable reports green.** `node --test src/board.test.mjs` exit 0 while `index.html` renders a blank page. | nothing links the verdict command to `writtenPaths`, computed 28 lines earlier at `:1102` and never consulted |
159
+ | 4 | **`attempts` is wrong.** Printed *"after 2 attempts"* for a command attempted once. | `attempts: runs.length` (`:1137`) is a session-wide count |
160
+
161
+ And it compounds with `ENTERPRISE.md` §3.5: a provider outage during the extension
162
+ round leaves `verification.passed === true` and prints `✔ VERIFIED` over a session that
163
+ died with work outstanding.
164
+
165
+ ⚠️ **Why this is the most expensive defect in the package.** `ENTERPRISE.md:531` sells
166
+ *"the exit code is a verdict"* as one of four things no competitor offers, and
167
+ `ENTERPRISE.md:60` sells `acuvo --json | jq '.verification.passed'` as a build step. Both
168
+ are currently false. `acuvo … && git push` believes a lie today.
169
+
170
+ ⚠️ **Two proposed fixes were tested and REFUTED — do not ship either.** "Zero bytes of
171
+ stdout is not a pass" misses the worst case entirely (a node:test harness that imports
172
+ `run()` against a fake io: exit 0, **16/16 pass, 1,787 bytes of stdout**, entrypoint
173
+ still inert) and false-fails `tsc --noEmit`, which prints nothing on success. Output
174
+ volume is not evidence.
175
+
176
+ **Fix size: hours.** Three parts, all using data already in scope at `:1130`:
177
+ reduce over `runs` keyed by command string keeping each distinct command's *latest*
178
+ result; add a fourth verdict state (`⚠ RAN, NOT VERIFIED`) for a green run that touches
179
+ nothing in `writtenPaths`; report per-command retry counts. Extend the stale-verdict
180
+ re-run at `:1078-1090` to re-run every still-failing command, not just the last one.
181
+
182
+ ### 2.2 ⚠️⚠️ #2 — The differentiator is dark by default, and broken when lit
183
+
184
+ Two independent defects stacked on the one capability `MVP-PLAN.md:23-28` calls the
185
+ whole positioning.
186
+
187
+ **(a) It is dark.** Measured by me today, in a normal shell:
188
+
189
+ ```
190
+ mediaToolNames({}) = []
191
+ mediaToolNames(process.env) = []
192
+ ```
193
+
194
+ `bin/acuvo.mjs` loads **no `.env` file anywhere** — grepping `bin/` and `lib/` for
195
+ `dotenv|.env.local|loadEnv` returns one unrelated code comment. `mediaConfig()`
196
+ (`lib/media.mjs:76`) gates on `RENDER_AUDIT_URL`/`MODAL_RENDER_AUDIT_URL`, which live
197
+ only in `console/.env.local:72`. With the tool absent, the entire design prompt block
198
+ (`systemPrompt`, `lib/turn.mjs`) is gated off too. **The model is never told sight
199
+ exists.**
200
+
201
+ > ⚠️ **THE QUOTE THAT USED TO BE ON THIS LINE IS GONE FROM THE CODE, AND IT WAS FALSE.**
202
+ > This paragraph quoted the block by its old heading, *"DESIGN — YOU CAN SEE WHAT YOU
203
+ > BUILT, AND NO OTHER TERMINAL AGENT CAN"*, citing `lib/turn.mjs:312-320`. Both the
204
+ > heading and the line numbers are stale as of 2026-08-11: Playwright MCP falsifies the
205
+ > claim in one `npx`, so the block now reads *"YOU CAN LOOK AT WHAT YOU BUILT, AND
206
+ > LOOKING IS CHEAP"* and states the true, more useful fact — the render comes back as
207
+ > ~89 tokens of measured problems rather than a 3,072-token screenshot. The observation
208
+ > below (the tool is gated off, so the model is never told) is unaffected and still
209
+ > stands.
210
+
211
+ **(b) When lit, it pays for the answer and throws it away.** `lib/media.mjs:143`, read
212
+ today, is `const m = res.json ?? {}` — then `:150` reads `m.screenshotPngB64`, `:158`
213
+ `m.viewport`, `:159` `m.findings`. The live service returns
214
+ `{ ok: true, measurement: { … } }`. **Every read is one level too shallow.**
215
+
216
+ The probe of the live endpoint with the actual generated file: HTTP 200, 52,113 bytes,
217
+ and inside `measurement`:
218
+
219
+ ```
220
+ consoleErrors : ["Failed to resolve module specifier \"./src/board.mjs\"…"]
221
+ paintedRatio : 0.0078 ← 0.8% of the viewport painted = blank page
222
+ screenshotPngB64: 51,472 chars
223
+ ```
224
+
225
+ `seePage` returned `{ok:true, screenshot:null, viewport:null, findings:[], looked:true}`.
226
+ There is **no `findings` key in the contract at all** — the findings *are*
227
+ `consoleErrors`/`paintedRatio`/`lowContrastText`/`clippedText`/`overlaps`/`brokenImages`,
228
+ and nothing maps them.
229
+
230
+ ⭐ **The comment at `lib/media.mjs:160-161` indicts the code above it.** It says the empty
231
+ array exists so *"I looked and it was fine"* is distinguishable from *"I could not look"*.
232
+ The shape mismatch creates a **third state the author did not anticipate** — looked,
233
+ paid, discarded everything — and it masquerades as the first. A silent empty findings
234
+ array is exactly the failure mode that comment was written to prevent.
235
+
236
+ Net observed effect: the model called `see_page` **unprompted**, Chromium ran, the bug
237
+ was named verbatim, the screenshot was bought — and the deliverable shipped blank with
238
+ `✔ VERIFIED`.
239
+
240
+ **Fix size: hours.** Unwrap the envelope; map the six real keys to findings; and make an
241
+ **unrecognised shape LOUD** — if none of the expected `measurement` keys are present,
242
+ return `{ok:false, error:'the render service returned an unrecognised shape'}` rather
243
+ than a clean-looking empty result. Add a contract test pinning `{ok, measurement:{…}}`
244
+ so the next endpoint change fails a test instead of a deliverable. Then load the URL
245
+ from something the CLI actually reads.
246
+
247
+ ### 2.3 ⚠️ #3 — A mid-stream timeout crashes with a raw stack and loses the entire round
248
+
249
+ **Root cause: `lib/model.mjs:272` vs `:297`** — confirmed by reading both today.
250
+ `signal: AbortSignal.timeout(timeoutMs)` is set on the fetch; the try/catch that converts
251
+ an abort into `{ok:false, error}` **closes at `:276`**, wrapping only `await fetchImpl(…)`.
252
+ An abort signal governs the response *body* too, so once headers arrive the guard is gone.
253
+ The timer fires mid-stream, undici errors the ReadableStream, and
254
+ `await collectStream(res.body, {onText})` at `:297` rethrows **outside the try**.
255
+ `lib/chain.mjs` has no try/catch at all; `lib/turn.mjs:884` is unguarded. It reaches
256
+ `bin/acuvo.mjs:274`: *"acuvo crashed — this is a bug in acuvo-code"*, exit 1, **zero files
257
+ written**.
258
+
259
+ Proven twice, including a zero-cost probe with two fake `fetchImpl`s on one 300ms timeout:
260
+ abort **before** headers → returned `{ok:false}`; abort **after** headers → **threw**.
261
+ Same signal, same timeout, opposite outcomes. The boundary is exactly the end of the try.
262
+
263
+ **Second, independent defect on the same path — verified by me today:**
264
+
265
+ ```
266
+ isRetryable('No response from OpenRouter within 180s — the call was aborted…') = false
267
+ ```
268
+
269
+ `describeTransportError` (`lib/model.mjs:149`) emits that string; `isRetryable`
270
+ (`lib/chain.mjs:78`) matches `/timed out|could not reach|network|ECONNRESET|…/`, none of
271
+ which appear in it. **The four-model fallback chain never fires on a timeout by either
272
+ path.**
273
+
274
+ ⚠️ Two corrections to the original report, both important: the *request* path is not
275
+ unguarded — the **stream** path is; and this is not tail-latency flakiness — the timeout
276
+ is a **total wall-clock budget** covering request plus entire stream, so any reply
277
+ exceeding 180s end-to-end fails **deterministically**. Long tasks fail more, by design,
278
+ which is precisely backwards.
279
+
280
+ **Fix size: hours.** Wrap the streaming branch (`:290-299`) in the same try/catch — that
281
+ alone converts the crash into a handled failure and lets existing partial-change
282
+ reporting run. Then replace the total-duration signal with an **idle** timer
283
+ `collectStream` resets per chunk (a stream may legitimately run past 180s; 180s of
284
+ *silence* must never happen). Then make timeouts retryable — better, return a structured
285
+ `{kind:'timeout'}` and have `isRetryable` switch on the kind instead of grepping prose.
286
+ The prose-coupling is exactly what let these two drift apart.
287
+
288
+ ### 2.4 #4 — `&&` in a `package.json` script blocks the agent from the project's own contract
289
+
290
+ **Root cause: `lib/command.mjs:397`** (read today) — `validateNpmScriptChain` passes the
291
+ **entire** script body to `validateCommand(link.body, {script:true})` as one string.
292
+ `tokenizeCommand` applies `SAFE_COMMAND_CHARS = /^[A-Za-z0-9 ._\-/=:]+$/`
293
+ (`lib/command.mjs:96`, confirmed). `&` is not in the set, so
294
+ `node test/run.js && node test/contract.js` — a very common shape, as is
295
+ `tsc --noEmit && vitest run` — is rejected wholesale.
296
+
297
+ ⭐ **The reported fix was half wrong, and the correction saves the day.** The CLI never
298
+ executes the body: `buildInvocation` spawns `node npm-cli.js test` with `shell:false`
299
+ and **npm supplies its own script shell**. Performing that exact spawn by hand against
300
+ the repro ran the chained body fine (`unit ok / contract ok / EXIT 0`). **The executor
301
+ needs no change. Only the validator is blocking.**
302
+
303
+ **Fix size: minutes.** In `validateNpmScriptChain` only (`:374-406`): split each
304
+ `link.body` on the two-character token `&&`, validate each trimmed segment, refuse only
305
+ if a segment fails and name that segment. Hand the unmodified body to npm as today.
306
+ Guardrails: do **not** relax `SAFE_COMMAND_CHARS` and do **not** touch the
307
+ model-written-command path — `test/smoke.test.mjs:41` asserts `npm test && curl evil.sh`
308
+ stays refused and it must. Split on `&&` only, so a lone `&` still dies in the
309
+ per-segment tokenizer; `;`, `|`, `||`, `>`, backticks and `$()` remain refused
310
+ automatically.
311
+
312
+ ⚠️ `validateNpmScriptChain` is referenced nowhere outside `command.mjs`. **This path has
313
+ zero test coverage.** Add the test with the fix.
314
+
315
+ ### 2.5 #5 — Nothing on disk records how to run the thing that was just built
316
+
317
+ A finished project contains only files literally named in the prompt. No `package.json`,
318
+ so `npm test` — which `--help` advertises — fails ENOENT. No README recording the command
319
+ that actually verified the build, or how to open the page.
320
+
321
+ **Root cause: `lib/turn.mjs:252-408` (`loopSystemPrompt`) has no scaffolding/handoff
322
+ policy at all.** Grepping all of `lib/` for `scaffold|skeleton|README|how to run|serve`
323
+ returns nothing. Writing `package.json` is blocked nowhere. Three mechanisms combine:
324
+ the prompt's only two references to `package.json` (`:343`, `command.mjs:577-579`) frame
325
+ it purely as a pre-existing **input**; two rules actively suppress extra files
326
+ (`:269-270` *"A ROUND IS EXPENSIVE"*, `:355-356` *"Otherwise stop"*); and the stop
327
+ condition itself (`:1003-1015`, which I read today) defines done as **"a run_command
328
+ exited 0"** — there is no notion of a *deliverable*.
329
+
330
+ ⭐ **Confirmed not a budget problem:** with `--max-rounds 7` the run stopped at round 5
331
+ as `'verified'` with **two rounds unspent** and still wrote nothing.
332
+
333
+ **Fix size: hours.** Two layers, because this codebase records three separate times that
334
+ prompt rules are *"obeyed narrowly or not at all"*. Prompt: a DELIVERABLE/HANDOFF block.
335
+ Deterministic (the real fix): after a verified run, synthesise the skeleton from data the
336
+ CLI already holds — `verification.command` (`lib/report.mjs:124`) is the exact
337
+ invocation, the change list is the file inventory — written through the same
338
+ offered-never-silently-written gate as `STARTER_TEMPLATE`. Plus a one-line quality fix at
339
+ `command.mjs:578`: *"no package.json in this workspace — write one first if you want npm
340
+ scripts"*, so the loop can recover unaided.
341
+
342
+ ### 2.6 Ranked summary
343
+
344
+ | # | weakness | root cause | observed failures | fix size |
345
+ |---|---|---|---|---|
346
+ | 1 | Verdict decided by one self-chosen command | `turn.mjs:1130` + `command.mjs:613` | **4** (+ `ENTERPRISE` §3.5) | hours |
347
+ | 2 | `see_page` dark by default, discards findings when lit | no env loader + `media.mjs:143` | **2**, incl. the flagship | hours |
348
+ | 3 | Mid-stream timeout crashes, never retries | `model.mjs:272` vs `:297`; `chain.mjs:79` | **1**, total work loss | hours |
349
+ | 4 | `&&` npm scripts refused | `command.mjs:397` | **1**, very common shape | **minutes** |
350
+ | 5 | No handoff scaffold | `turn.mjs:252-408` | **1** | hours |
351
+
352
+ ---
353
+
354
+ ## 3. ⭐ Does the "horizon not IQ" thesis survive?
355
+
356
+ **The claim under test:** every failure is a budget ceiling — rounds, tokens, context —
357
+ rather than a model limitation.
358
+
359
+ ### 3.1 It is refuted for the observed failure set, and the refutation is direct
360
+
361
+ **Zero of the seven confirmed root causes is horizon-bound.** Every one carries
362
+ `is_horizon: false`, and four were reproduced *with the model removed entirely* (injected
363
+ stubs, no API spend) — a defect you can reproduce without a model cannot be a model or
364
+ budget defect.
365
+
366
+ Three pieces of evidence are decisive rather than merely suggestive:
367
+
368
+ 1. **The controlled budget experiment came back negative.** The horizon agent ran the
369
+ identical prompt at `--max-tokens 8000` and `--max-tokens 16000`, same 8 rounds, fresh
370
+ dirs. **Both exited 0, both printed `✔ VERIFIED`, both shipped an inert CLI.**
371
+ Doubling the budget changed the cost and changed nothing else. That is the thesis
372
+ tested on its own terms.
373
+
374
+ 2. **Three failing runs stopped with budget in hand.** The scaffold failure stopped at
375
+ round 5 of 7. The inert-program run stopped at round 3 of 6. Both exited via
376
+ `stoppedBecause='verified'`, never `'round-cap'`. **They did not run out of room; they
377
+ were told they had finished.**
378
+
379
+ 3. **The failures are in our code, at named lines.** A response-shape mismatch
380
+ (`media.mjs:143`), a try/catch closing brace (`model.mjs:276`), an array index
381
+ (`turn.mjs:1130`), a character class (`command.mjs:96`). None of these gets better
382
+ with a bigger model or a bigger budget.
383
+
384
+ Meanwhile the *successes* also refute it from the other side: the kanban app landed in
385
+ **3 rounds**, the real bug was found in **one round of looking**. Neither needed horizon
386
+ either.
387
+
388
+ ### 3.2 The thesis was pointing at the wrong axis — there is a third category
389
+
390
+ Horizon-vs-IQ is a **two-axis frame and the data lands off both axes.** The controlling
391
+ variable in all seven cases is a third thing:
392
+
393
+ > ⭐ **The harness is the ceiling.** The model called `see_page` unprompted and correctly;
394
+ > we discarded the result. The model decomposed a chained npm script under refusal; we
395
+ > refused it for no executable reason. The model diagnosed its own Windows path bug and
396
+ > its own `io.file ?? parsed.file` precedence bug; we then scored the session on an
397
+ > unrelated command. **In every observed failure the agent's judgement was sound and our
398
+ > plumbing was not.**
399
+
400
+ This is a genuinely better position than either alternative. Horizon problems cost money
401
+ forever; IQ problems require waiting for someone else's model. **Plumbing problems are
402
+ ours, they are cheap, and four of the five ranked items are "hours".**
403
+
404
+ ### 3.3 ⚠️ Where the data is honestly too thin to tell
405
+
406
+ Say this plainly rather than over-claiming the refutation:
407
+
408
+ - **Nothing tested a task that genuinely needs 20+ rounds.** `ENTERPRISE.md:491` claims
409
+ *"a refactor that needs twenty tool rounds cannot be expressed here"* and **no evidence
410
+ above touches it.** The largest observed task was 6 files. The horizon thesis is
411
+ refuted **for tasks in the 3–8 round band**; for genuinely long work it is *untested*,
412
+ not disproven. Do not cite this document as having settled it.
413
+ - **`is_horizon: false` on the inert-program cases is inferential.** They stopped as
414
+ `'verified'` because the verdict is broken — so we cannot know whether more rounds
415
+ would have helped, because the loop was never told it had failed. **Fix §2.1 first,
416
+ then re-run the horizon bench.** That measurement is only meaningful afterwards.
417
+ - **One model, one provider.** Everything ran on `deepseek/deepseek-v4-flash-0731`. The
418
+ IQ half of the thesis has not been tested at all — nobody ran the same corpus on a
419
+ frontier model. `ENTERPRISE.md:494-498` concedes the point honestly and this evidence
420
+ neither supports nor contradicts it.
421
+
422
+ **Verdict: REFUTED for the observed set, with the horizon question for large tasks
423
+ still open — and unmeasurable until §2.1 lands.**
424
+
425
+ ---
426
+
427
+ ## 4. Build order for the next two weeks
428
+
429
+ Numbered, each justified by an observation above. Nothing here is on the
430
+ `ENTERPRISE.md` §4 security list or the `MVP-PLAN.md` §7 cut line; both run in parallel
431
+ and neither is superseded.
432
+
433
+ ### Week 1 — make the output true
434
+
435
+ 1. **Fix the verdict reduction (`turn.mjs:1130`).** *Hours.*
436
+ → Unblocks four observed failures, more than anything else on this list, and it is
437
+ load-bearing for the two headline claims in `ENTERPRISE.md` (§1.3 build step, §5
438
+ "exit code is a verdict"). Ship the reduce-over-`runs` and the per-command `attempts`
439
+ together; add the `⚠ RAN, NOT VERIFIED` fourth state in the same commit. Test:
440
+ fail-then-pass across rounds must yield `passed:false` and non-zero exit.
441
+
442
+ 2. **Split `&&` in `validateNpmScriptChain` (`command.mjs:397`).** *Minutes.*
443
+ → Observed to cost a paid round in a real run, on the most common test-script shape
444
+ there is. Cheapest confirmed fix in the package, and the executor needs no change at
445
+ all. **Add the missing test** — this path has zero coverage today.
446
+
447
+ 3. **Guard the stream read and make timeouts retryable (`model.mjs:290-299`,
448
+ `chain.mjs:79`).** *Hours.*
449
+ → Observed as a raw stack trace, exit 1, **zero files written**, and the fallback
450
+ chain provably never fires (`isRetryable(…) = false`, measured). Ship the try/catch
451
+ extension first — it converts a crash into a handled failure on its own — then the
452
+ idle timer, then the structured `{kind:'timeout'}`.
453
+
454
+ 4. **Load env, then unwrap the `see_page` envelope (`media.mjs:143-162`).** *Hours.*
455
+ → The tool `MVP-PLAN.md:23` calls the entire positioning returns `[]` in a real
456
+ terminal, and returns nothing useful even when configured. Ship in this order: env
457
+ loading (else the fix is invisible), envelope unwrap, findings mapping from the six
458
+ real keys, **loud failure on unrecognised shape**, contract test on
459
+ `{ok, measurement:{…}}`.
460
+
461
+ 5. **Re-run the horizon bench.** *Hours.*
462
+ → §3.3: the horizon question is unmeasurable while a broken verdict ends runs early
463
+ as `'verified'`. Once 1 and 4 land, re-run the same two prompts at 8k/16k tokens. If
464
+ the CLI now reports honest failure and *then* runs out of rounds, the thesis becomes
465
+ testable for the first time. **Publish the result either way.**
466
+
467
+ ### Week 2 — make the loop finish the job
468
+
469
+ 6. **Deterministic handoff scaffold after a verified run.** *Hours.*
470
+ → Observed: a run stopped with two rounds unspent and wrote no `package.json`, so
471
+ `npm test` ENOENTs on a project the CLI just built. Synthesise from
472
+ `verification.command` + the change list, behind the existing
473
+ offered-never-silently-written gate. Plus the one-line refusal improvement at
474
+ `command.mjs:578`.
475
+
476
+ 7. **Scope the verdict to the deliverable.** *Hours.*
477
+ → Observed: a green `node --test` over a blank `index.html`. When an `.html` file was
478
+ written and never rendered, say so instead of an unqualified `✔ VERIFIED`; flag a page
479
+ whose only script is a bare relative ES module as `file://`-unopenable. This is the
480
+ half of §2.1 that needs §2.2 landed first — hence week 2.
481
+
482
+ 8. **Fix `MVP-PLAN.md` §3 (`--json`, test count).** *Minutes.*
483
+ → §0. A plan that under-reports its own coverage stops being consulted.
484
+
485
+ 9. **The regression corpus, checked in.** *Hours.*
486
+ → Five agents produced ~25 verified runs and **not one is reproducible from this
487
+ repo.** Every observation in this document came from a temp dir that no longer
488
+ exists. Check in the kanban prompt, the orders-api fixture with its planted
489
+ off-by-one, the inert-program case and the chained-npm-script case as fixtures with
490
+ expected verdicts. Otherwise week 3 re-discovers week 1.
491
+
492
+ ⚠️ **Explicitly not in these two weeks:** parallel tasks, sub-agents, colour, PR opening,
493
+ entitlements, an audit log. Every one is real and every one is downstream of a loop whose
494
+ verdict currently lies.
495
+
496
+ ---
497
+
498
+ ## 5. What would have to be true for this to be a serious technology product
499
+
500
+ Unsentimental. The tool is good. A tool is not a product.
501
+
502
+ **What is already true and genuinely rare.** The safety boundary is small enough to read
503
+ in an afternoon and it survived 33 adversarial cases. The write-run-fix loop works on
504
+ real code and was verified by people trying to disprove it. It makes PDFs and speech from
505
+ one prompt. Zero dependencies. Roughly a fifth of a cent per task. That combination does
506
+ not exist elsewhere and it is worth defending.
507
+
508
+ **What has to become true.**
509
+
510
+ 1. **The exit code must be trustworthy — not usually, always.** Everything else here is a
511
+ feature; this is the *contract*. Two docs already sell it and four observed failures
512
+ break it. Until fixed, every claim in this repo rests on a number that lies. It is
513
+ also the cheapest item on the list, which makes shipping without it inexcusable.
514
+
515
+ 2. **The differentiator must be on by default.** *"It can see"* is the entire wedge
516
+ (`MVP-PLAN.md:23-28`), and in a plain terminal `mediaToolNames(process.env)` is `[]`.
517
+ A capability that requires knowing which of two `.env.local` files to source is not a
518
+ product feature; it is an internal demo. This must work on a stranger's laptop, first
519
+ run, with a documented setup line — or it is not a differentiator, it is a story.
520
+
521
+ 3. **A polyglot answer, or an honest narrowing.** `ALLOWED_BINARIES` is `node`, `npm`,
522
+ `npx`, `tsc`. A Python, Go, Rust or Java shop cannot run a single test, which degrades
523
+ the one thing that works into "writes files and cannot check them"
524
+ (`ENTERPRISE.md` §5.1). Two defensible paths: **add languages behind per-language
525
+ argument grammars** (the safety argument survives, the work is real), or **declare it
526
+ the JS/TS tool** and stop pretending otherwise. Both are serious. Leaving it ambiguous
527
+ is not.
528
+
529
+ 4. **Reproducible numbers that someone else can run.** Every number in this document came
530
+ from a temp directory that no longer exists. A serious product ships a corpus, a
531
+ command, and a published result. ⭐ **Nobody in this category publishes reproducible
532
+ benchmarks.** Being first is credibility marketing cannot buy — and it is the natural
533
+ home for the honest horizon-vs-IQ answer §3.3 says we do not yet have.
534
+
535
+ 5. **A distribution decision, made rather than deferred.** BYOK means the tool is free and
536
+ earns nothing (`MVP-PLAN.md:151-153`). Fine for adoption, fatal as an oversight. And a
537
+ BYOK user running Cline on their own DeepSeek key pays the same price we do — so
538
+ **"cheaper" is not the pitch, "it can see and it makes PDFs" is.** Decide, write it
539
+ down, price against *that*.
540
+
541
+ 6. **The failure modes must be as well-engineered as the refusals.** The refusal messages
542
+ are the best-written thing in the package and they are why the agent recovers instead
543
+ of flailing. The crash path is a raw stack trace. A product's worst day is what it is
544
+ judged on.
545
+
546
+ 7. **Zero users, still.** `MVP-PLAN.md:209` says it and it remains the only sentence that
547
+ matters. **Nothing in this document distributes anything.** Weeks 1–2 make the tool
548
+ honest, which is a prerequisite for showing it to anyone — not a substitute for doing
549
+ so. The GitHub repo in the README still does not exist, and it is the first command a
550
+ stranger runs.
551
+
552
+ ⭐ **The one-line version.** Acuvo Code is a good tool whose agent outperforms its
553
+ plumbing, whose headline capability is switched off, and whose verdict does not yet mean
554
+ what two documents say it means. Fix those three and the honest pitch — *the terminal
555
+ agent that can look at what it built, and whose exit code you can put in a build script*
556
+ — becomes true. It is not true today, and all three fixes are measured in hours.