tumwater 0.4.0 → 0.4.2

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 (291) hide show
  1. package/README.md +4 -6
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-backend.js +5 -2
  4. package/dist/src/agent/backend-traits.js +21 -0
  5. package/dist/src/agent/claude/claude-backend.js +1 -1
  6. package/dist/src/agent/claude/claude-stream.js +181 -182
  7. package/dist/src/agent/claude/{write-guard-hook.js → claude-write-guard-hook.js} +1 -1
  8. package/dist/src/agent/claude/claude-write-guard.js +5 -4
  9. package/dist/src/agent/codex/codex-backend.js +1 -1
  10. package/dist/src/agent/codex/codex-rollout.js +48 -45
  11. package/dist/src/agent/codex/codex-stream.js +269 -215
  12. package/dist/src/agent/codex/{write-guard-hook.js → codex-write-guard-hook.js} +2 -1
  13. package/dist/src/agent/codex/codex-write-guard.js +3 -3
  14. package/dist/src/agent/image-input.js +18 -87
  15. package/dist/src/agent/notes-shim.js +2 -2
  16. package/dist/src/agent/opencode/opencode-backend.js +2 -2
  17. package/dist/src/agent/opencode/opencode-catalog.js +2 -2
  18. package/dist/src/agent/opencode/opencode-config.js +2 -2
  19. package/dist/src/agent/opencode/opencode-stream.js +101 -92
  20. package/dist/src/agent/run-agent.js +76 -382
  21. package/dist/src/agent/run-settle.js +25 -15
  22. package/dist/src/agent/run-spawn.js +226 -0
  23. package/dist/src/agent/run-stream.js +185 -0
  24. package/dist/src/agent/session-ledger.js +16 -0
  25. package/dist/src/agent/write-guard.js +5 -3
  26. package/dist/src/backlog/backlog-archive.js +86 -170
  27. package/dist/src/backlog/backlog-cli-view.js +89 -70
  28. package/dist/src/backlog/backlog-eligibility.js +51 -59
  29. package/dist/src/backlog/backlog-file-bugs.js +133 -0
  30. package/dist/src/backlog/backlog-index-data.js +159 -0
  31. package/dist/src/backlog/backlog-index.js +7 -5
  32. package/dist/src/backlog/backlog-md.js +39 -28
  33. package/dist/src/backlog/backlog-release.js +21 -36
  34. package/dist/src/backlog/backlog-render.js +1 -1
  35. package/dist/src/backlog/backlog-structure.js +14 -145
  36. package/dist/src/backlog/backlog-write.js +31 -18
  37. package/dist/src/backlog/backlog.js +14 -4
  38. package/dist/src/backlog/question-answer.js +33 -15
  39. package/dist/src/baseline/main-baseline.js +71 -26
  40. package/dist/src/baseline/main-guardrails.js +76 -15
  41. package/dist/src/baseline/main-red.js +45 -94
  42. package/dist/src/baseline/main-watch.js +9 -3
  43. package/dist/src/brief/brief.js +40 -0
  44. package/dist/src/budget/budget-fallback-view.js +50 -8
  45. package/dist/src/budget/budget-wiring.js +15 -11
  46. package/dist/src/budget/budget.js +3 -2
  47. package/dist/src/build/build-check-events.js +4 -2
  48. package/dist/src/build/build-check-report.js +2 -1
  49. package/dist/src/build/build-check-scoped.js +70 -41
  50. package/dist/src/change/change-data.js +4 -2
  51. package/dist/src/cli/cli-args.js +9 -0
  52. package/dist/src/cli/cli-command-args.js +199 -98
  53. package/dist/src/cli/cli-flag-specs.js +6 -4
  54. package/dist/src/cli/cli-marker-commands.js +2 -2
  55. package/dist/src/cli/cli-query-commands.js +41 -6
  56. package/dist/src/cli/cli-review-commands.js +2 -1
  57. package/dist/src/cli/cli-role-command.js +46 -92
  58. package/dist/src/cli/cli-start.js +123 -47
  59. package/dist/src/cli/follow-view.js +3 -1
  60. package/dist/src/cli/help.js +19 -12
  61. package/dist/src/cli/log-commands.js +128 -107
  62. package/dist/src/cli/question-commands.js +82 -63
  63. package/dist/src/cli.js +396 -343
  64. package/dist/src/collections.js +46 -9
  65. package/dist/src/config/config-fallbacks.js +0 -4
  66. package/dist/src/config/config-live.js +179 -147
  67. package/dist/src/config/config-request.js +176 -51
  68. package/dist/src/config/config-validation-roles.js +271 -0
  69. package/dist/src/config/config-validation.js +10 -205
  70. package/dist/src/config/config-views.js +2 -7
  71. package/dist/src/config/config-write.js +205 -175
  72. package/dist/src/config/config.js +2 -1
  73. package/dist/src/doctor/doctor-backlog-bugs.js +24 -122
  74. package/dist/src/doctor/doctor-backlog-plans.js +24 -25
  75. package/dist/src/doctor/doctor-backlog.js +34 -42
  76. package/dist/src/doctor/doctor-check-names.js +4 -2
  77. package/dist/src/doctor/doctor-checks.js +16 -4
  78. package/dist/src/doctor/doctor-claude-checks.js +2 -2
  79. package/dist/src/doctor/doctor-codex-checks.js +9 -20
  80. package/dist/src/doctor/doctor-contention-checks.js +52 -19
  81. package/dist/src/doctor/doctor-holds.js +22 -20
  82. package/dist/src/doctor/doctor-model-checks.js +8 -184
  83. package/dist/src/doctor/doctor-notify.js +2 -5
  84. package/dist/src/doctor/doctor-opencode-checks.js +90 -41
  85. package/dist/src/doctor/doctor-principles.js +26 -0
  86. package/dist/src/doctor/doctor-project-checks.js +69 -19
  87. package/dist/src/doctor/doctor-role-notes.js +33 -0
  88. package/dist/src/doctor/doctor.js +13 -6
  89. package/dist/src/doctor/probe.js +1 -1
  90. package/dist/src/events/event-format.js +307 -162
  91. package/dist/src/events/event-labels.js +0 -1
  92. package/dist/src/events/event-legacy.js +1 -1
  93. package/dist/src/events/event-window.js +1 -1
  94. package/dist/src/events/events.js +15 -2
  95. package/dist/src/events/notify.js +1 -1
  96. package/dist/src/failure/failure-cluster.js +4 -3
  97. package/dist/src/failure/failure-data.js +126 -94
  98. package/dist/src/failure/failure-render.js +58 -17
  99. package/dist/src/failure/failure-state-change.js +308 -27
  100. package/dist/src/failure/time-spend.js +139 -106
  101. package/dist/src/files/files.js +7 -0
  102. package/dist/src/files/json-object.js +15 -2
  103. package/dist/src/gates/budget-gates.js +90 -68
  104. package/dist/src/gates/gate-polls.js +92 -61
  105. package/dist/src/gates/role-cap-gates.js +1 -1
  106. package/dist/src/gates/startup-gate.js +28 -28
  107. package/dist/src/gates/streak-gate.js +1 -1
  108. package/dist/src/git/git.js +1 -1
  109. package/dist/src/git/slot-salvage.js +9 -9
  110. package/dist/src/git/slots-state.js +12 -3
  111. package/dist/src/git/worktree-pool-startup.js +127 -0
  112. package/dist/src/git/worktree-pool.js +1 -120
  113. package/dist/src/gui/gui-admin-endpoints.js +3 -4
  114. package/dist/src/gui/gui-args.js +1 -1
  115. package/dist/src/gui/gui-backlog-commands.js +20 -11
  116. package/dist/src/gui/gui-backlog-endpoints.js +2 -8
  117. package/dist/src/gui/gui-config-commands.js +51 -76
  118. package/dist/src/gui/gui-endpoint-commands.js +4 -2
  119. package/dist/src/gui/gui-endpoint-common.js +4 -3
  120. package/dist/src/gui/gui-history-endpoints.js +36 -27
  121. package/dist/src/gui/gui-operator-commands.js +43 -17
  122. package/dist/src/gui/gui-prompt-commands.js +27 -21
  123. package/dist/src/gui/gui-quarantine-commands.js +30 -0
  124. package/dist/src/gui/gui-report-endpoints.js +13 -9
  125. package/dist/src/gui/gui-server.js +5 -3
  126. package/dist/src/gui/gui-team-commands.js +1 -1
  127. package/dist/src/history/history-data.js +7 -1
  128. package/dist/src/history/history-render.js +15 -10
  129. package/dist/src/history/history.js +103 -68
  130. package/dist/src/inbox/inbox-submit.js +1 -5
  131. package/dist/src/inbox/pending-prompt.js +3 -2
  132. package/dist/src/inbox/prompt-commands.js +194 -161
  133. package/dist/src/init/init.js +104 -25
  134. package/dist/src/merge/merge-core.js +1 -1
  135. package/dist/src/merge/merge-drain.js +1 -1
  136. package/dist/src/merge/merge-git.js +5 -5
  137. package/dist/src/merge/{merge-post-review.js → merge-post-review-drain.js} +1 -1
  138. package/dist/src/merge/merge-queue.js +2 -1
  139. package/dist/src/merge/merge-to-main.js +1 -1
  140. package/dist/src/merge/merge-verify.js +3 -2
  141. package/dist/src/operator/operator-commands.js +84 -52
  142. package/dist/src/operator/operator-intent.js +26 -3
  143. package/dist/src/operator/retire.js +2 -1
  144. package/dist/src/operator/revert.js +18 -12
  145. package/dist/src/orchestrator/orchestrator-poll.js +2 -2
  146. package/dist/src/orchestrator/orchestrator.js +1 -1
  147. package/dist/src/orchestrator/retention.js +6 -5
  148. package/dist/src/paths.js +10 -10
  149. package/dist/src/pi/pi-bin.js +2 -2
  150. package/dist/src/pi/pi-models.js +9 -0
  151. package/dist/src/pi/pi-stream.js +63 -41
  152. package/dist/src/pi/pi-watchdogs.js +84 -49
  153. package/dist/src/pi-extension/context-shake.js +55 -31
  154. package/dist/src/pi-extension/full-output.js +2 -3
  155. package/dist/src/process/process.js +24 -1
  156. package/dist/src/process/run-marker.js +2 -2
  157. package/dist/src/process/supervisor.js +24 -5
  158. package/dist/src/prompt/gate-notes.js +41 -16
  159. package/dist/src/prompt/notes-backlog.js +22 -29
  160. package/dist/src/prompt/principles.js +11 -0
  161. package/dist/src/prompt/prompt-followup.js +4 -12
  162. package/dist/src/prompt/prompt.js +15 -12
  163. package/dist/src/redeploy/redeploy-probes.js +11 -1
  164. package/dist/src/redeploy/redeployer.js +54 -7
  165. package/dist/src/redeploy/self-reload.js +7 -2
  166. package/dist/src/report/report-data.js +224 -320
  167. package/dist/src/report/report-fold.js +23 -231
  168. package/dist/src/report/report-main-red.js +70 -3
  169. package/dist/src/report/report-render.js +181 -48
  170. package/dist/src/report/report-since-data.js +218 -0
  171. package/dist/src/report/report-usage-fold.js +288 -0
  172. package/dist/src/report/report.js +2 -1
  173. package/dist/src/review/known-flakes.js +22 -3
  174. package/dist/src/review/post-merge-review-accept.js +3 -2
  175. package/dist/src/review/post-merge-review.js +16 -14
  176. package/dist/src/review/quarantine-render.js +29 -11
  177. package/dist/src/review/review-precheck.js +2 -1
  178. package/dist/src/roles/role-catalog.js +30 -29
  179. package/dist/src/roles/role-guidance.js +26 -43
  180. package/dist/src/roles/role-render.js +19 -0
  181. package/dist/src/roles/role-view.js +19 -5
  182. package/dist/src/roles/roles.js +2 -22
  183. package/dist/src/run/ci-parity.js +5 -6
  184. package/dist/src/run/feedback-filings.js +10 -1
  185. package/dist/src/run/qa-coverage.js +4 -4
  186. package/dist/src/run/run-apply.js +14 -2
  187. package/dist/src/run/run-detail-data.js +50 -10
  188. package/dist/src/run/run-detail-render.js +32 -1
  189. package/dist/src/run/run-detail.js +44 -22
  190. package/dist/src/run/run-dismiss.js +43 -0
  191. package/dist/src/run/run-finalize.js +75 -46
  192. package/dist/src/run/run-prompt.js +271 -187
  193. package/dist/src/run/run-resume.js +32 -31
  194. package/dist/src/run/run-stage.js +116 -64
  195. package/dist/src/run/run-timing.js +18 -4
  196. package/dist/src/run/run-verdict.js +119 -83
  197. package/dist/src/run/stage-backlog-checks.js +4 -31
  198. package/dist/src/run/stage-check.js +4 -12
  199. package/dist/src/run/stage-checks-shared.js +15 -0
  200. package/dist/src/run/stage-name-checks.js +3 -13
  201. package/dist/src/run/stage-path-checks.js +26 -29
  202. package/dist/src/runner/leftover.js +13 -4
  203. package/dist/src/runner/revision.js +2 -2
  204. package/dist/src/runner/runner-fallback.js +1 -1
  205. package/dist/src/runner/runner-outcome.js +0 -5
  206. package/dist/src/runner/runner-pi.js +72 -49
  207. package/dist/src/runner/runner-run.js +19 -102
  208. package/dist/src/runner/runner-state-load.js +0 -10
  209. package/dist/src/runner/runner.js +1 -32
  210. package/dist/src/scheduling/claims.js +4 -11
  211. package/dist/src/status/status-data.js +140 -91
  212. package/dist/src/status/status-polls.js +41 -5
  213. package/dist/src/status/status-role-notes.js +53 -0
  214. package/dist/src/status/status-roles.js +10 -37
  215. package/dist/src/text/datetime.js +8 -1
  216. package/dist/src/text/format.js +13 -1
  217. package/dist/src/text/phrases.js +16 -2
  218. package/dist/src/text/text.js +12 -0
  219. package/dist/src/ui/alert-feeds.js +115 -0
  220. package/dist/src/ui/badges.js +3 -4
  221. package/dist/src/ui/gui/gui-client-boot.js +78 -57
  222. package/dist/src/ui/gui/gui-client-drawer-config.js +8 -3
  223. package/dist/src/ui/gui/gui-client-drawer.js +18 -2
  224. package/dist/src/ui/gui/gui-client-feed.js +3 -8
  225. package/dist/src/ui/gui/gui-client-history.js +16 -16
  226. package/dist/src/ui/gui/gui-client-model.js +9 -9
  227. package/dist/src/ui/gui/gui-client-operator.js +6 -10
  228. package/dist/src/ui/gui/gui-client-pending.js +4 -4
  229. package/dist/src/ui/gui/gui-client-report-chart.js +111 -10
  230. package/dist/src/ui/gui/gui-client-report.js +70 -11
  231. package/dist/src/ui/gui/gui-client-roles.js +52 -12
  232. package/dist/src/ui/gui/gui-client-settings.js +31 -26
  233. package/dist/src/ui/gui/gui-client-team.js +28 -2
  234. package/dist/src/ui/gui/gui-client.js +33 -4
  235. package/dist/src/ui/gui/gui-icons.js +7 -1
  236. package/dist/src/ui/progress-data.js +2 -2
  237. package/dist/src/ui/run-progress-model.js +2 -2
  238. package/dist/src/ui/status-model.js +26 -16
  239. package/dist/src/ui/status-payload.js +149 -326
  240. package/dist/src/ui/status-render.js +131 -80
  241. package/dist/src/ui/team-alert-backlog.js +236 -0
  242. package/dist/src/ui/team-alert-shared.js +92 -0
  243. package/dist/src/ui/team-alert-status.js +326 -0
  244. package/dist/src/ui/team-alert-tail.js +238 -0
  245. package/dist/src/ui/team-alerts.js +16 -984
  246. package/dist/src/ui/transcript-tail.js +137 -94
  247. package/dist/src/ui/tui/tui-backlog.js +1 -1
  248. package/dist/src/ui/tui/tui-editor-answer.js +54 -44
  249. package/dist/src/ui/tui/tui-editor-attach.js +74 -63
  250. package/dist/src/ui/tui/tui-editor-backends.js +62 -56
  251. package/dist/src/ui/tui/tui-editor-brief.js +64 -54
  252. package/dist/src/ui/tui/tui-editor-budget.js +106 -89
  253. package/dist/src/ui/tui/tui-editor-config.js +2 -4
  254. package/dist/src/ui/tui/tui-editor-ctx.js +12 -1
  255. package/dist/src/ui/tui/tui-editor-entry.js +171 -153
  256. package/dist/src/ui/tui/tui-editor-instances.js +2 -1
  257. package/dist/src/ui/tui/tui-editor-instructions.js +50 -107
  258. package/dist/src/ui/tui/tui-editor-interval.js +39 -112
  259. package/dist/src/ui/tui/tui-editor-notes.js +28 -84
  260. package/dist/src/ui/tui/tui-editor-pause.js +210 -183
  261. package/dist/src/ui/tui/tui-editor-prompt.js +46 -35
  262. package/dist/src/ui/tui/tui-editor-queued.js +74 -64
  263. package/dist/src/ui/tui/tui-editor-quiet.js +122 -108
  264. package/dist/src/ui/tui/tui-editor-release.js +85 -0
  265. package/dist/src/ui/tui/tui-editor-retire.js +57 -46
  266. package/dist/src/ui/tui/tui-editor-role-enabled.js +90 -80
  267. package/dist/src/ui/tui/tui-editor-role-file.js +87 -0
  268. package/dist/src/ui/tui/tui-editor-role-key.js +50 -42
  269. package/dist/src/ui/tui/tui-editor-search.js +70 -58
  270. package/dist/src/ui/tui/tui-editor-session.js +3 -0
  271. package/dist/src/ui/tui/tui-editors.js +4 -1
  272. package/dist/src/ui/tui/tui-frame.js +7 -3
  273. package/dist/src/ui/tui/tui-history.js +12 -10
  274. package/dist/src/ui/tui/tui-keys.js +6 -1
  275. package/dist/src/ui/tui/tui-pane-activity.js +62 -50
  276. package/dist/src/ui/tui/tui-pane-backlog.js +4 -4
  277. package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
  278. package/dist/src/ui/tui/tui-pane-health.js +45 -26
  279. package/dist/src/ui/tui/tui-pane-history.js +296 -264
  280. package/dist/src/ui/tui/tui-pane-report.js +74 -57
  281. package/dist/src/ui/tui/tui-pane-session.js +6 -2
  282. package/dist/src/ui/tui/tui-pane-transcript.js +158 -131
  283. package/dist/src/ui/tui/tui-pane.js +201 -174
  284. package/dist/src/ui/tui/tui-panes.js +211 -148
  285. package/dist/src/ui/tui/tui.js +4 -4
  286. package/dist/src/verdict/fix-claim.js +18 -6
  287. package/dist/src/verdict/refusal.js +32 -7
  288. package/package.json +1 -1
  289. package/dist/src/doctor/image-probe.js +0 -257
  290. /package/dist/src/gui/{http-body.js → gui-http-body.js} +0 -0
  291. /package/dist/src/merge/{backlog-conflicts.js → merge-backlog-conflicts.js} +0 -0
@@ -1,257 +0,0 @@
1
- /** The doctor's live image-input probe (plans/image-input.md "Doctor", part 2/5): a claimed
2
- * image-input capability in pi's models.json is only a claim — doctor shows each claimed model
3
- * a real image and checks the answer. This module owns three things: the in-process PNG
4
- * generator (a random 4-digit code drawn with a built-in 5×7 bitmap, encoded with
5
- * `zlib.deflateSync` + `zlib.crc32` is newer than the `>=20.3` node floor, so the CRC-32 here is
6
- * hand-rolled), the probe runner (the resolved pi binary in a run's environment, bounded like
7
- * the doctor's other binary probes), and the `.tumwater/state/image-probes.json` cache the
8
- * doctor check reads back. No new dependency; runs entirely offline against the operator's pi. */
9
- import fs from "node:fs";
10
- import os from "node:os";
11
- import path from "node:path";
12
- import { randomInt } from "node:crypto";
13
- import zlib from "node:zlib";
14
- import { DAY_MS } from "../text/datetime.js";
15
- import { PiStreamParser } from "../pi/pi-stream.js";
16
- import { resolveBackendBin } from "../pi/pi-bin.js";
17
- import { imageProbesPath } from "../paths.js";
18
- import { removeTree } from "../files/files.js";
19
- import { readJsonFile, writeJsonAtomic } from "../files/json-files.js";
20
- import { isJsonObject, validEntries } from "../files/json-object.js";
21
- import { boundedProbe } from "./probe.js";
22
- /** How long a cached image-probe verdict stays valid while the claim is unchanged (7 days) —
23
- * a model's endpoint can change underneath a stale pass, so the answer is dated. */
24
- export const IMAGE_PROBE_TTL_MS = 7 * DAY_MS;
25
- /** The probe's deadline (90 s): a local model may need to load, far beyond the quick credential
26
- * probes, but a doctor run is still a pre-flight — a hung backend must lose, not stall the
27
- * report. */
28
- const PROBE_TIMEOUT_MS = 90_000;
29
- /** Cap on the probe's captured stdout, the same guard as the auth probes: a doctor run must not
30
- * grow an unbounded buffer off a runaway log. */
31
- const PROBE_MAX_OUTPUT_BYTES = 1024 * 1024;
32
- /** The cache-file key for one seam — agent/provider/model, the natural identity of a team
33
- * model, joined on `/` (agent and provider never contain it and a model id is fully qualified,
34
- * so the key cannot collide with another seam's). */
35
- export function imageProbeKey(agent, provider, model) {
36
- return [agent ?? "", provider ?? "", model ?? ""].join("/");
37
- }
38
- /** Whether a cached entry still stands for the current claim: it exists, it recorded a real
39
- * verdict (never `could-not-verify`, which is never cached), it tested the image-input claim,
40
- * and it is younger than the TTL. */
41
- export function imageProbeEntryFresh(entry, now = Date.now()) {
42
- return (entry !== undefined &&
43
- (entry.verdict === "pass" || entry.verdict === "fail") &&
44
- entry.claim &&
45
- now - entry.at <= IMAGE_PROBE_TTL_MS);
46
- }
47
- /** Whether a parsed cache entry has the shape of an `ImageProbeCacheEntry`. */
48
- function isImageProbeCacheEntry(value) {
49
- return (isJsonObject(value) &&
50
- (value.verdict === "pass" || value.verdict === "fail") &&
51
- typeof value.detail === "string" &&
52
- typeof value.at === "number" &&
53
- Number.isFinite(value.at) &&
54
- typeof value.claim === "boolean");
55
- }
56
- /** Read the probe cache, or an empty cache when the file is missing or torn (the harness's
57
- * no-data policy: a crashed writer never takes down a reader). An entry of a foreign shape is
58
- * dropped, so it reads as unprobed, and the doctor's next cache write leaves it out. */
59
- export function readImageProbeCache(root) {
60
- return validEntries(readJsonFile(imageProbesPath(root)), isImageProbeCacheEntry);
61
- }
62
- /** Write the probe cache atomically (tmp+rename) so a concurrent reader never sees a torn
63
- * file. */
64
- export function writeImageProbeCache(root, cache) {
65
- writeJsonAtomic(imageProbesPath(root), cache);
66
- }
67
- // ── PNG generator ─────────────────────────────────────────────────────────────────────────
68
- /** Each digit as 7 rows of 5 bits, '1' = drawn (black); the standard 5×7 seven-segment-ish
69
- * font, big enough for a vision model to read and small enough to stay a trivial payload. */
70
- const DIGIT_BITMAP = {
71
- "0": ["01110", "10001", "10011", "10101", "11001", "10001", "01110"],
72
- "1": ["00100", "01100", "00100", "00100", "00100", "00100", "01110"],
73
- "2": ["01110", "10001", "00001", "00110", "01000", "10000", "11111"],
74
- "3": ["11111", "00010", "00100", "00010", "00001", "10001", "01110"],
75
- "4": ["00010", "00110", "01010", "10010", "11111", "00010", "00010"],
76
- "5": ["11111", "10000", "11110", "00001", "00001", "10001", "01110"],
77
- "6": ["00110", "01000", "10000", "11110", "10001", "10001", "01110"],
78
- "7": ["11111", "00001", "00010", "00100", "01000", "01000", "01000"],
79
- "8": ["01110", "10001", "10001", "01110", "10001", "10001", "01110"],
80
- "9": ["01110", "10001", "10001", "01111", "00001", "00010", "01100"],
81
- };
82
- /** A hand-rolled CRC-32 (the PNG chunk checksum): `zlib.crc32` is newer than the `>=20.3` node
83
- * floor, so this bitwise table version is the one home of the polynomial. */
84
- const CRC32_TABLE = (() => {
85
- const table = new Uint32Array(256);
86
- for (let n = 0; n < 256; n++) {
87
- let c = n;
88
- for (let k = 0; k < 8; k++)
89
- c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
90
- table[n] = c >>> 0;
91
- }
92
- return table;
93
- })();
94
- function crc32(buf) {
95
- let c = 0xffffffff;
96
- for (let i = 0; i < buf.length; i++)
97
- c = (CRC32_TABLE[(c ^ (buf[i] ?? 0)) & 0xff] ?? 0) ^ (c >>> 8);
98
- return (c ^ 0xffffffff) >>> 0;
99
- }
100
- /** One PNG chunk: 4-byte big-endian length, the 4-byte ASCII type, the data, and the CRC over
101
- * type+data. */
102
- function pngChunk(type, data) {
103
- const len = Buffer.alloc(4);
104
- len.writeUInt32BE(data.length, 0);
105
- const typeBuf = Buffer.from(type, "ascii");
106
- const crc = Buffer.alloc(4);
107
- crc.writeUInt32BE(crc32(Buffer.concat([typeBuf, data])), 0);
108
- return Buffer.concat([len, typeBuf, data, crc]);
109
- }
110
- /** A PNG showing `code` black on white, drawn with the 5×7 bitmap scaled `scale`-fold (each
111
- * font cell becomes a `scale`×`scale` block) and one `scale`-wide gap between digits. Truecolor
112
- * (color type 2) so any reader handles it and the decode-check in the tests is trivial. Exported
113
- * for tests (and the doctor's own validity check of a generated image). */
114
- export function makeProbePng(code, scale = 8) {
115
- const digits = [...code].map((d) => DIGIT_BITMAP[d] ?? DIGIT_BITMAP["0"]);
116
- const width = digits.length * 5 * scale + (digits.length - 1) * scale;
117
- const height = 7 * scale;
118
- // Truecolor RGB scanlines, each prefixed with filter byte 0 (none).
119
- const raw = Buffer.alloc(height * (1 + width * 3));
120
- for (let y = 0; y < height; y++) {
121
- const rowStart = y * (1 + width * 3);
122
- raw[rowStart] = 0;
123
- for (let x = 0; x < width; x++) {
124
- // Which font cell does this pixel fall in?
125
- const cell = Math.floor(x / (5 * scale + scale));
126
- const inCellX = x - cell * (5 * scale + scale);
127
- const cellDigit = digits[cell];
128
- const row = Math.floor(y / scale);
129
- const col = Math.floor(inCellX / scale);
130
- const bit = inCellX < 5 * scale && cellDigit !== undefined
131
- ? ((cellDigit[row] ?? "")[col] ?? "")
132
- : "";
133
- const on = bit === "1";
134
- const p = rowStart + 1 + x * 3;
135
- raw[p] = on ? 0 : 255;
136
- raw[p + 1] = on ? 0 : 255;
137
- raw[p + 2] = on ? 0 : 255;
138
- }
139
- }
140
- const ihdr = Buffer.alloc(13);
141
- ihdr.writeUInt32BE(width, 0);
142
- ihdr.writeUInt32BE(height, 4);
143
- ihdr[8] = 8; // bit depth
144
- ihdr[9] = 2; // color type: truecolor RGB
145
- const idat = zlib.deflateSync(raw, { level: 9 });
146
- return Buffer.concat([
147
- Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
148
- pngChunk("IHDR", ihdr),
149
- pngChunk("IDAT", idat),
150
- pngChunk("IEND", Buffer.alloc(0)),
151
- ]);
152
- }
153
- /** A random 4-digit code for one probe attempt. */
154
- function randomCode() {
155
- let out = "";
156
- for (let i = 0; i < 4; i++)
157
- out += String(randomInt(0, 10));
158
- return out;
159
- }
160
- // ── Probe runner ──────────────────────────────────────────────────────────────────────────
161
- /** Parse the resolved probe's stdout (pi's `--mode json` event stream) the same way a real run
162
- * is parsed, so a live probe sees the same final text and error classification the harness
163
- * would. */
164
- function parseProbeOutput(stdout) {
165
- const parser = new PiStreamParser();
166
- parser.feed(stdout);
167
- return {
168
- finalText: parser.finalText,
169
- configError: parser.configError,
170
- transientRateLimit: parser.transientRateLimit,
171
- transientBackend: parser.transientBackend,
172
- };
173
- }
174
- /** Whether an answer looks like a (wrong) read of the 4-digit code rather than a refusal: the
175
- * text contains a 4-digit run of digits (e.g. "The number is 5825."). A model that cannot see
176
- * at all does not emit a number. */
177
- function isFourDigitNumber(text) {
178
- return /\b\d{4}\b/.test(text);
179
- }
180
- /** Probe one claimed pi seam with a real image (plans/image-input.md "Doctor"). Runs the
181
- * resolved pi binary in a run's environment (the caller passes `env`, typically
182
- * `childEnv({ PATH: pathEnv })` which inherits `PI_CODING_AGENT_DIR`, so the probe reads the
183
- * same models.json a real run would):
184
- *
185
- * `pi --print --mode json --no-session --no-tools --no-mcp --provider P --model M
186
- * --thinking <level> @<png> "Reply with only the 4-digit number shown in the image."`
187
- *
188
- * Pass: the final assistant text contains the code. A wrong answer retries once with a fresh
189
- * code (a misread is not blindness). Two wrong answers that are not both numbers fail (a
190
- * refusal or empty answer is blindness), and so does a 4xx. Two wrong *numbers* — the model
191
- * read a number, wrongly — cannot be told from a flaky misread, so it is `could-not-verify`
192
- * rather than `fail` and is never cached: the next doctor run probes again instead of turning
193
- * a capable seam text-only for a week. A timeout, spawn failure, 429, 5xx, or connection error
194
- * is also `could-not-verify` — it says nothing about capability and is never cached. `code` is
195
- * a test seam (a real probe always draws a fresh random code); `env` and `timeoutMs` are
196
- * injectable so tests need no real pi. */
197
- export async function runImageProbe(config, seam, env, timeoutMs = PROBE_TIMEOUT_MS, code) {
198
- const envBin = env.TUMWATER_PI_BIN?.trim();
199
- // Resolve from the run's own environment (env override → config), exactly as a real run's
200
- // spawn would rather than only the harness process env — so tests can point a fake pi there.
201
- const bin = envBin
202
- ? envBin.includes("/")
203
- ? path.resolve(envBin)
204
- : envBin
205
- : resolveBackendBin(config).bin;
206
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), "tumwater-image-probe-"));
207
- const wrong = []; // the per-attempt "answered …" detail for a wrong answer
208
- const misread = []; // whether each wrong answer was a (wrong) 4-digit number
209
- try {
210
- for (let attempt = 0; attempt < 2; attempt++) {
211
- const thisCode = code ?? randomCode();
212
- const png = makeProbePng(thisCode);
213
- const pngPath = path.join(dir, `probe-${attempt}.png`);
214
- fs.writeFileSync(pngPath, png);
215
- const args = ["--print", "--mode", "json", "--no-session", "--no-tools", "--no-mcp"];
216
- if (seam.provider)
217
- args.push("--provider", seam.provider);
218
- if (seam.model)
219
- args.push("--model", seam.model);
220
- if (seam.thinking)
221
- args.push("--thinking", seam.thinking);
222
- args.push(`@${pngPath}`, "Reply with only the 4-digit number shown in the image.");
223
- const result = await boundedProbe(bin, args, env, timeoutMs, PROBE_MAX_OUTPUT_BYTES);
224
- if (result === null)
225
- return { verdict: "could-not-verify", detail: "the probe could not run or timed out" };
226
- const parsed = parseProbeOutput(result.stdout);
227
- if (parsed.configError)
228
- return { verdict: "fail", detail: "the provider rejected the request (4xx)" };
229
- if (parsed.transientRateLimit)
230
- return { verdict: "could-not-verify", detail: "the provider rate-limited the request (429)" };
231
- if (parsed.transientBackend)
232
- return { verdict: "could-not-verify", detail: "the provider backend failed (5xx or connection)" };
233
- const text = parsed.finalText.trim();
234
- if (text.includes(thisCode))
235
- return { verdict: "pass", detail: `answered "${text}"` };
236
- // A wrong 4-digit number says "I looked and got the digits wrong"; a refusal or empty
237
- // answer says "I cannot see". The drawn code is exactly 4 digits, so a 4-digit answer is
238
- // a plausible (wrong) read.
239
- misread.push(isFourDigitNumber(text));
240
- wrong.push(text ? `answered "${text}"` : "answered nothing");
241
- }
242
- // A refusal or empty answer on either attempt is blindness — the two-strikes rule: fail,
243
- // cached. Two wrong *numbers* cannot be told from a flaky misread, so it is could-not-verify
244
- // and never cached: the next doctor run probes again instead of turning a capable seam
245
- // text-only for a week. Either way the detail names what the model said, so the doctor
246
- // line, the alert, and a bug report can tell blindness from a misread.
247
- return misread[0] && misread[1]
248
- ? {
249
- verdict: "could-not-verify",
250
- detail: `two misreads (${wrong.join(", ")}) — the model read a number, wrongly; not blindness, re-probe on the next doctor run`,
251
- }
252
- : { verdict: "fail", detail: `two wrong answers (${wrong[1]})` };
253
- }
254
- finally {
255
- removeTree(dir);
256
- }
257
- }
File without changes