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.
- package/README.md +4 -6
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-backend.js +5 -2
- package/dist/src/agent/backend-traits.js +21 -0
- package/dist/src/agent/claude/claude-backend.js +1 -1
- package/dist/src/agent/claude/claude-stream.js +181 -182
- package/dist/src/agent/claude/{write-guard-hook.js → claude-write-guard-hook.js} +1 -1
- package/dist/src/agent/claude/claude-write-guard.js +5 -4
- package/dist/src/agent/codex/codex-backend.js +1 -1
- package/dist/src/agent/codex/codex-rollout.js +48 -45
- package/dist/src/agent/codex/codex-stream.js +269 -215
- package/dist/src/agent/codex/{write-guard-hook.js → codex-write-guard-hook.js} +2 -1
- package/dist/src/agent/codex/codex-write-guard.js +3 -3
- package/dist/src/agent/image-input.js +18 -87
- package/dist/src/agent/notes-shim.js +2 -2
- package/dist/src/agent/opencode/opencode-backend.js +2 -2
- package/dist/src/agent/opencode/opencode-catalog.js +2 -2
- package/dist/src/agent/opencode/opencode-config.js +2 -2
- package/dist/src/agent/opencode/opencode-stream.js +101 -92
- package/dist/src/agent/run-agent.js +76 -382
- package/dist/src/agent/run-settle.js +25 -15
- package/dist/src/agent/run-spawn.js +226 -0
- package/dist/src/agent/run-stream.js +185 -0
- package/dist/src/agent/session-ledger.js +16 -0
- package/dist/src/agent/write-guard.js +5 -3
- package/dist/src/backlog/backlog-archive.js +86 -170
- package/dist/src/backlog/backlog-cli-view.js +89 -70
- package/dist/src/backlog/backlog-eligibility.js +51 -59
- package/dist/src/backlog/backlog-file-bugs.js +133 -0
- package/dist/src/backlog/backlog-index-data.js +159 -0
- package/dist/src/backlog/backlog-index.js +7 -5
- package/dist/src/backlog/backlog-md.js +39 -28
- package/dist/src/backlog/backlog-release.js +21 -36
- package/dist/src/backlog/backlog-render.js +1 -1
- package/dist/src/backlog/backlog-structure.js +14 -145
- package/dist/src/backlog/backlog-write.js +31 -18
- package/dist/src/backlog/backlog.js +14 -4
- package/dist/src/backlog/question-answer.js +33 -15
- package/dist/src/baseline/main-baseline.js +71 -26
- package/dist/src/baseline/main-guardrails.js +76 -15
- package/dist/src/baseline/main-red.js +45 -94
- package/dist/src/baseline/main-watch.js +9 -3
- package/dist/src/brief/brief.js +40 -0
- package/dist/src/budget/budget-fallback-view.js +50 -8
- package/dist/src/budget/budget-wiring.js +15 -11
- package/dist/src/budget/budget.js +3 -2
- package/dist/src/build/build-check-events.js +4 -2
- package/dist/src/build/build-check-report.js +2 -1
- package/dist/src/build/build-check-scoped.js +70 -41
- package/dist/src/change/change-data.js +4 -2
- package/dist/src/cli/cli-args.js +9 -0
- package/dist/src/cli/cli-command-args.js +199 -98
- package/dist/src/cli/cli-flag-specs.js +6 -4
- package/dist/src/cli/cli-marker-commands.js +2 -2
- package/dist/src/cli/cli-query-commands.js +41 -6
- package/dist/src/cli/cli-review-commands.js +2 -1
- package/dist/src/cli/cli-role-command.js +46 -92
- package/dist/src/cli/cli-start.js +123 -47
- package/dist/src/cli/follow-view.js +3 -1
- package/dist/src/cli/help.js +19 -12
- package/dist/src/cli/log-commands.js +128 -107
- package/dist/src/cli/question-commands.js +82 -63
- package/dist/src/cli.js +396 -343
- package/dist/src/collections.js +46 -9
- package/dist/src/config/config-fallbacks.js +0 -4
- package/dist/src/config/config-live.js +179 -147
- package/dist/src/config/config-request.js +176 -51
- package/dist/src/config/config-validation-roles.js +271 -0
- package/dist/src/config/config-validation.js +10 -205
- package/dist/src/config/config-views.js +2 -7
- package/dist/src/config/config-write.js +205 -175
- package/dist/src/config/config.js +2 -1
- package/dist/src/doctor/doctor-backlog-bugs.js +24 -122
- package/dist/src/doctor/doctor-backlog-plans.js +24 -25
- package/dist/src/doctor/doctor-backlog.js +34 -42
- package/dist/src/doctor/doctor-check-names.js +4 -2
- package/dist/src/doctor/doctor-checks.js +16 -4
- package/dist/src/doctor/doctor-claude-checks.js +2 -2
- package/dist/src/doctor/doctor-codex-checks.js +9 -20
- package/dist/src/doctor/doctor-contention-checks.js +52 -19
- package/dist/src/doctor/doctor-holds.js +22 -20
- package/dist/src/doctor/doctor-model-checks.js +8 -184
- package/dist/src/doctor/doctor-notify.js +2 -5
- package/dist/src/doctor/doctor-opencode-checks.js +90 -41
- package/dist/src/doctor/doctor-principles.js +26 -0
- package/dist/src/doctor/doctor-project-checks.js +69 -19
- package/dist/src/doctor/doctor-role-notes.js +33 -0
- package/dist/src/doctor/doctor.js +13 -6
- package/dist/src/doctor/probe.js +1 -1
- package/dist/src/events/event-format.js +307 -162
- package/dist/src/events/event-labels.js +0 -1
- package/dist/src/events/event-legacy.js +1 -1
- package/dist/src/events/event-window.js +1 -1
- package/dist/src/events/events.js +15 -2
- package/dist/src/events/notify.js +1 -1
- package/dist/src/failure/failure-cluster.js +4 -3
- package/dist/src/failure/failure-data.js +126 -94
- package/dist/src/failure/failure-render.js +58 -17
- package/dist/src/failure/failure-state-change.js +308 -27
- package/dist/src/failure/time-spend.js +139 -106
- package/dist/src/files/files.js +7 -0
- package/dist/src/files/json-object.js +15 -2
- package/dist/src/gates/budget-gates.js +90 -68
- package/dist/src/gates/gate-polls.js +92 -61
- package/dist/src/gates/role-cap-gates.js +1 -1
- package/dist/src/gates/startup-gate.js +28 -28
- package/dist/src/gates/streak-gate.js +1 -1
- package/dist/src/git/git.js +1 -1
- package/dist/src/git/slot-salvage.js +9 -9
- package/dist/src/git/slots-state.js +12 -3
- package/dist/src/git/worktree-pool-startup.js +127 -0
- package/dist/src/git/worktree-pool.js +1 -120
- package/dist/src/gui/gui-admin-endpoints.js +3 -4
- package/dist/src/gui/gui-args.js +1 -1
- package/dist/src/gui/gui-backlog-commands.js +20 -11
- package/dist/src/gui/gui-backlog-endpoints.js +2 -8
- package/dist/src/gui/gui-config-commands.js +51 -76
- package/dist/src/gui/gui-endpoint-commands.js +4 -2
- package/dist/src/gui/gui-endpoint-common.js +4 -3
- package/dist/src/gui/gui-history-endpoints.js +36 -27
- package/dist/src/gui/gui-operator-commands.js +43 -17
- package/dist/src/gui/gui-prompt-commands.js +27 -21
- package/dist/src/gui/gui-quarantine-commands.js +30 -0
- package/dist/src/gui/gui-report-endpoints.js +13 -9
- package/dist/src/gui/gui-server.js +5 -3
- package/dist/src/gui/gui-team-commands.js +1 -1
- package/dist/src/history/history-data.js +7 -1
- package/dist/src/history/history-render.js +15 -10
- package/dist/src/history/history.js +103 -68
- package/dist/src/inbox/inbox-submit.js +1 -5
- package/dist/src/inbox/pending-prompt.js +3 -2
- package/dist/src/inbox/prompt-commands.js +194 -161
- package/dist/src/init/init.js +104 -25
- package/dist/src/merge/merge-core.js +1 -1
- package/dist/src/merge/merge-drain.js +1 -1
- package/dist/src/merge/merge-git.js +5 -5
- package/dist/src/merge/{merge-post-review.js → merge-post-review-drain.js} +1 -1
- package/dist/src/merge/merge-queue.js +2 -1
- package/dist/src/merge/merge-to-main.js +1 -1
- package/dist/src/merge/merge-verify.js +3 -2
- package/dist/src/operator/operator-commands.js +84 -52
- package/dist/src/operator/operator-intent.js +26 -3
- package/dist/src/operator/retire.js +2 -1
- package/dist/src/operator/revert.js +18 -12
- package/dist/src/orchestrator/orchestrator-poll.js +2 -2
- package/dist/src/orchestrator/orchestrator.js +1 -1
- package/dist/src/orchestrator/retention.js +6 -5
- package/dist/src/paths.js +10 -10
- package/dist/src/pi/pi-bin.js +2 -2
- package/dist/src/pi/pi-models.js +9 -0
- package/dist/src/pi/pi-stream.js +63 -41
- package/dist/src/pi/pi-watchdogs.js +84 -49
- package/dist/src/pi-extension/context-shake.js +55 -31
- package/dist/src/pi-extension/full-output.js +2 -3
- package/dist/src/process/process.js +24 -1
- package/dist/src/process/run-marker.js +2 -2
- package/dist/src/process/supervisor.js +24 -5
- package/dist/src/prompt/gate-notes.js +41 -16
- package/dist/src/prompt/notes-backlog.js +22 -29
- package/dist/src/prompt/principles.js +11 -0
- package/dist/src/prompt/prompt-followup.js +4 -12
- package/dist/src/prompt/prompt.js +15 -12
- package/dist/src/redeploy/redeploy-probes.js +11 -1
- package/dist/src/redeploy/redeployer.js +54 -7
- package/dist/src/redeploy/self-reload.js +7 -2
- package/dist/src/report/report-data.js +224 -320
- package/dist/src/report/report-fold.js +23 -231
- package/dist/src/report/report-main-red.js +70 -3
- package/dist/src/report/report-render.js +181 -48
- package/dist/src/report/report-since-data.js +218 -0
- package/dist/src/report/report-usage-fold.js +288 -0
- package/dist/src/report/report.js +2 -1
- package/dist/src/review/known-flakes.js +22 -3
- package/dist/src/review/post-merge-review-accept.js +3 -2
- package/dist/src/review/post-merge-review.js +16 -14
- package/dist/src/review/quarantine-render.js +29 -11
- package/dist/src/review/review-precheck.js +2 -1
- package/dist/src/roles/role-catalog.js +30 -29
- package/dist/src/roles/role-guidance.js +26 -43
- package/dist/src/roles/role-render.js +19 -0
- package/dist/src/roles/role-view.js +19 -5
- package/dist/src/roles/roles.js +2 -22
- package/dist/src/run/ci-parity.js +5 -6
- package/dist/src/run/feedback-filings.js +10 -1
- package/dist/src/run/qa-coverage.js +4 -4
- package/dist/src/run/run-apply.js +14 -2
- package/dist/src/run/run-detail-data.js +50 -10
- package/dist/src/run/run-detail-render.js +32 -1
- package/dist/src/run/run-detail.js +44 -22
- package/dist/src/run/run-dismiss.js +43 -0
- package/dist/src/run/run-finalize.js +75 -46
- package/dist/src/run/run-prompt.js +271 -187
- package/dist/src/run/run-resume.js +32 -31
- package/dist/src/run/run-stage.js +116 -64
- package/dist/src/run/run-timing.js +18 -4
- package/dist/src/run/run-verdict.js +119 -83
- package/dist/src/run/stage-backlog-checks.js +4 -31
- package/dist/src/run/stage-check.js +4 -12
- package/dist/src/run/stage-checks-shared.js +15 -0
- package/dist/src/run/stage-name-checks.js +3 -13
- package/dist/src/run/stage-path-checks.js +26 -29
- package/dist/src/runner/leftover.js +13 -4
- package/dist/src/runner/revision.js +2 -2
- package/dist/src/runner/runner-fallback.js +1 -1
- package/dist/src/runner/runner-outcome.js +0 -5
- package/dist/src/runner/runner-pi.js +72 -49
- package/dist/src/runner/runner-run.js +19 -102
- package/dist/src/runner/runner-state-load.js +0 -10
- package/dist/src/runner/runner.js +1 -32
- package/dist/src/scheduling/claims.js +4 -11
- package/dist/src/status/status-data.js +140 -91
- package/dist/src/status/status-polls.js +41 -5
- package/dist/src/status/status-role-notes.js +53 -0
- package/dist/src/status/status-roles.js +10 -37
- package/dist/src/text/datetime.js +8 -1
- package/dist/src/text/format.js +13 -1
- package/dist/src/text/phrases.js +16 -2
- package/dist/src/text/text.js +12 -0
- package/dist/src/ui/alert-feeds.js +115 -0
- package/dist/src/ui/badges.js +3 -4
- package/dist/src/ui/gui/gui-client-boot.js +78 -57
- package/dist/src/ui/gui/gui-client-drawer-config.js +8 -3
- package/dist/src/ui/gui/gui-client-drawer.js +18 -2
- package/dist/src/ui/gui/gui-client-feed.js +3 -8
- package/dist/src/ui/gui/gui-client-history.js +16 -16
- package/dist/src/ui/gui/gui-client-model.js +9 -9
- package/dist/src/ui/gui/gui-client-operator.js +6 -10
- package/dist/src/ui/gui/gui-client-pending.js +4 -4
- package/dist/src/ui/gui/gui-client-report-chart.js +111 -10
- package/dist/src/ui/gui/gui-client-report.js +70 -11
- package/dist/src/ui/gui/gui-client-roles.js +52 -12
- package/dist/src/ui/gui/gui-client-settings.js +31 -26
- package/dist/src/ui/gui/gui-client-team.js +28 -2
- package/dist/src/ui/gui/gui-client.js +33 -4
- package/dist/src/ui/gui/gui-icons.js +7 -1
- package/dist/src/ui/progress-data.js +2 -2
- package/dist/src/ui/run-progress-model.js +2 -2
- package/dist/src/ui/status-model.js +26 -16
- package/dist/src/ui/status-payload.js +149 -326
- package/dist/src/ui/status-render.js +131 -80
- package/dist/src/ui/team-alert-backlog.js +236 -0
- package/dist/src/ui/team-alert-shared.js +92 -0
- package/dist/src/ui/team-alert-status.js +326 -0
- package/dist/src/ui/team-alert-tail.js +238 -0
- package/dist/src/ui/team-alerts.js +16 -984
- package/dist/src/ui/transcript-tail.js +137 -94
- package/dist/src/ui/tui/tui-backlog.js +1 -1
- package/dist/src/ui/tui/tui-editor-answer.js +54 -44
- package/dist/src/ui/tui/tui-editor-attach.js +74 -63
- package/dist/src/ui/tui/tui-editor-backends.js +62 -56
- package/dist/src/ui/tui/tui-editor-brief.js +64 -54
- package/dist/src/ui/tui/tui-editor-budget.js +106 -89
- package/dist/src/ui/tui/tui-editor-config.js +2 -4
- package/dist/src/ui/tui/tui-editor-ctx.js +12 -1
- package/dist/src/ui/tui/tui-editor-entry.js +171 -153
- package/dist/src/ui/tui/tui-editor-instances.js +2 -1
- package/dist/src/ui/tui/tui-editor-instructions.js +50 -107
- package/dist/src/ui/tui/tui-editor-interval.js +39 -112
- package/dist/src/ui/tui/tui-editor-notes.js +28 -84
- package/dist/src/ui/tui/tui-editor-pause.js +210 -183
- package/dist/src/ui/tui/tui-editor-prompt.js +46 -35
- package/dist/src/ui/tui/tui-editor-queued.js +74 -64
- package/dist/src/ui/tui/tui-editor-quiet.js +122 -108
- package/dist/src/ui/tui/tui-editor-release.js +85 -0
- package/dist/src/ui/tui/tui-editor-retire.js +57 -46
- package/dist/src/ui/tui/tui-editor-role-enabled.js +90 -80
- package/dist/src/ui/tui/tui-editor-role-file.js +87 -0
- package/dist/src/ui/tui/tui-editor-role-key.js +50 -42
- package/dist/src/ui/tui/tui-editor-search.js +70 -58
- package/dist/src/ui/tui/tui-editor-session.js +3 -0
- package/dist/src/ui/tui/tui-editors.js +4 -1
- package/dist/src/ui/tui/tui-frame.js +7 -3
- package/dist/src/ui/tui/tui-history.js +12 -10
- package/dist/src/ui/tui/tui-keys.js +6 -1
- package/dist/src/ui/tui/tui-pane-activity.js +62 -50
- package/dist/src/ui/tui/tui-pane-backlog.js +4 -4
- package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
- package/dist/src/ui/tui/tui-pane-health.js +45 -26
- package/dist/src/ui/tui/tui-pane-history.js +296 -264
- package/dist/src/ui/tui/tui-pane-report.js +74 -57
- package/dist/src/ui/tui/tui-pane-session.js +6 -2
- package/dist/src/ui/tui/tui-pane-transcript.js +158 -131
- package/dist/src/ui/tui/tui-pane.js +201 -174
- package/dist/src/ui/tui/tui-panes.js +211 -148
- package/dist/src/ui/tui/tui.js +4 -4
- package/dist/src/verdict/fix-claim.js +18 -6
- package/dist/src/verdict/refusal.js +32 -7
- package/package.json +1 -1
- package/dist/src/doctor/image-probe.js +0 -257
- /package/dist/src/gui/{http-body.js → gui-http-body.js} +0 -0
- /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
|
|
File without changes
|