@giamme/forge 0.0.0-stage → 0.9.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 (152) hide show
  1. package/CHANGELOG.md +205 -0
  2. package/LICENSE +21 -0
  3. package/README.md +194 -2
  4. package/SKILL.md +123 -0
  5. package/VERSION +1 -0
  6. package/docs/README.md +26 -0
  7. package/docs/add-ons.md +33 -0
  8. package/docs/artifacts.md +69 -0
  9. package/docs/commands.md +315 -0
  10. package/docs/dataset.md +20 -0
  11. package/docs/decomposed-runs.md +268 -0
  12. package/docs/development.md +95 -0
  13. package/docs/forge-ui.md +92 -0
  14. package/docs/fractal.md +181 -0
  15. package/docs/install.md +160 -0
  16. package/docs/memory.md +156 -0
  17. package/docs/models.md +118 -0
  18. package/docs/reviews.md +231 -0
  19. package/docs/safety.md +55 -0
  20. package/docs/troubleshooting.md +54 -0
  21. package/forge +79 -0
  22. package/package.json +27 -4
  23. package/references/dataset.md +329 -0
  24. package/references/decompose.md +667 -0
  25. package/references/fractal-dashboard.md +21 -0
  26. package/references/fractal.md +307 -0
  27. package/references/harnesses.md +365 -0
  28. package/references/jev.md +865 -0
  29. package/references/memory.md +228 -0
  30. package/references/planning.md +42 -0
  31. package/references/ripwire.md +47 -0
  32. package/references/runtime.md +64 -0
  33. package/references/solo.md +113 -0
  34. package/references/ui.md +112 -0
  35. package/registry.tsv +81 -0
  36. package/scripts/forge-artifact.sh +78 -0
  37. package/scripts/forge-contract.py +430 -0
  38. package/scripts/forge-dataset.py +6 -0
  39. package/scripts/forge-dispatch.sh +671 -0
  40. package/scripts/forge-fail-first.py +195 -0
  41. package/scripts/forge-fractal-dispatch.sh +4 -0
  42. package/scripts/forge-fractal-options.sh +54 -0
  43. package/scripts/forge-fractal.py +6 -0
  44. package/scripts/forge-guard.py +266 -0
  45. package/scripts/forge-install-fractal.sh +4 -0
  46. package/scripts/forge-install-ripwire.sh +52 -0
  47. package/scripts/forge-install.sh +135 -0
  48. package/scripts/forge-jev-options.sh +259 -0
  49. package/scripts/forge-jev.py +6 -0
  50. package/scripts/forge-lib-plans.sh +785 -0
  51. package/scripts/forge-lib-review.sh +494 -0
  52. package/scripts/forge-lib-task.sh +302 -0
  53. package/scripts/forge-lib-verify.sh +30 -0
  54. package/scripts/forge-memory.sh +575 -0
  55. package/scripts/forge-metrics.sh +39 -0
  56. package/scripts/forge-parallel-options.sh +76 -0
  57. package/scripts/forge-parallel.sh +1717 -0
  58. package/scripts/forge-plan-check.py +118 -0
  59. package/scripts/forge-prompt.py +45 -0
  60. package/scripts/forge-ripwire.py +136 -0
  61. package/scripts/forge-route.py +344 -0
  62. package/scripts/forge-runs.py +121 -0
  63. package/scripts/forge-runtime.py +586 -0
  64. package/scripts/forge-schedule.py +312 -0
  65. package/scripts/forge-shadow.py +305 -0
  66. package/scripts/forge-solo.sh +347 -0
  67. package/scripts/forge-ui-options.sh +42 -0
  68. package/scripts/forge-ui.py +435 -0
  69. package/scripts/forge-update.sh +71 -0
  70. package/scripts/forge-verify-coverage.py +956 -0
  71. package/scripts/forge-version.py +240 -0
  72. package/scripts/forge_dataset/__init__.py +439 -0
  73. package/scripts/forge_dataset/capture.py +330 -0
  74. package/scripts/forge_dataset/checks.py +109 -0
  75. package/scripts/forge_dataset/cli.py +233 -0
  76. package/scripts/forge_dataset/export.py +1009 -0
  77. package/scripts/forge_dataset/harvest.py +716 -0
  78. package/scripts/forge_dataset/importer.py +412 -0
  79. package/scripts/forge_dataset/outcomes.py +275 -0
  80. package/scripts/forge_dataset/routing.py +278 -0
  81. package/scripts/forge_fractal/__init__.py +84 -0
  82. package/scripts/forge_fractal/bridge.py +46 -0
  83. package/scripts/forge_fractal/cli.py +202 -0
  84. package/scripts/forge_fractal/dashboard.css +138 -0
  85. package/scripts/forge_fractal/dashboard.html +41 -0
  86. package/scripts/forge_fractal/dashboard.js +687 -0
  87. package/scripts/forge_fractal/decomposition.py +262 -0
  88. package/scripts/forge_fractal/execution.py +659 -0
  89. package/scripts/forge_fractal/inspection.py +261 -0
  90. package/scripts/forge_fractal/install.py +158 -0
  91. package/scripts/forge_fractal/invocation.py +94 -0
  92. package/scripts/forge_fractal/pipeline.py +124 -0
  93. package/scripts/forge_fractal/selection.py +179 -0
  94. package/scripts/forge_jev/__init__.py +376 -0
  95. package/scripts/forge_jev/backtest.py +322 -0
  96. package/scripts/forge_jev/cli.py +980 -0
  97. package/scripts/forge_jev/client.py +418 -0
  98. package/scripts/forge_jev/corpus.py +156 -0
  99. package/scripts/forge_jev/coupling.py +244 -0
  100. package/scripts/forge_jev/features.py +24 -0
  101. package/scripts/forge_jev/gates.py +100 -0
  102. package/scripts/forge_jev/memory.py +163 -0
  103. package/scripts/forge_jev/outcome.py +49 -0
  104. package/scripts/forge_jev/predict.py +204 -0
  105. package/scripts/forge_jev/questions.py +661 -0
  106. package/scripts/forge_jev/review.py +73 -0
  107. package/scripts/forge_jev/routing.py +271 -0
  108. package/scripts/forge_jev/subset.py +123 -0
  109. package/scripts/forge_jev/verify.py +453 -0
  110. package/scripts/forge_registry.py +140 -0
  111. package/ui/README.md +347 -0
  112. package/ui/THIRD_PARTY_NOTICES.md +177 -0
  113. package/ui/docs/api.md +970 -0
  114. package/ui/forge_ui/__init__.py +20 -0
  115. package/ui/forge_ui/__main__.py +135 -0
  116. package/ui/forge_ui/diffs.py +454 -0
  117. package/ui/forge_ui/executions.py +436 -0
  118. package/ui/forge_ui/export.py +214 -0
  119. package/ui/forge_ui/findings.py +132 -0
  120. package/ui/forge_ui/fractal.py +582 -0
  121. package/ui/forge_ui/models.py +216 -0
  122. package/ui/forge_ui/reader.py +1424 -0
  123. package/ui/forge_ui/server.py +312 -0
  124. package/ui/forge_ui/static/D-DINCondensed-Bold.woff2 +0 -0
  125. package/ui/forge_ui/static/app.js +1079 -0
  126. package/ui/forge_ui/static/cloud3d.js +647 -0
  127. package/ui/forge_ui/static/cloudmodel.js +375 -0
  128. package/ui/forge_ui/static/cloudui.js +397 -0
  129. package/ui/forge_ui/static/convo.js +295 -0
  130. package/ui/forge_ui/static/datatable.js +232 -0
  131. package/ui/forge_ui/static/diffview.js +235 -0
  132. package/ui/forge_ui/static/favicon.svg +1 -0
  133. package/ui/forge_ui/static/files.js +776 -0
  134. package/ui/forge_ui/static/findings3d.js +134 -0
  135. package/ui/forge_ui/static/gantt.js +97 -0
  136. package/ui/forge_ui/static/graph.js +266 -0
  137. package/ui/forge_ui/static/icons.js +94 -0
  138. package/ui/forge_ui/static/index.html +67 -0
  139. package/ui/forge_ui/static/iso.js +816 -0
  140. package/ui/forge_ui/static/leaderboard.js +311 -0
  141. package/ui/forge_ui/static/map3d.js +45 -0
  142. package/ui/forge_ui/static/modelstats.js +249 -0
  143. package/ui/forge_ui/static/notify.js +179 -0
  144. package/ui/forge_ui/static/panel.js +562 -0
  145. package/ui/forge_ui/static/runmodel.js +667 -0
  146. package/ui/forge_ui/static/scene3d.js +1471 -0
  147. package/ui/forge_ui/static/search.js +120 -0
  148. package/ui/forge_ui/static/shell.js +137 -0
  149. package/ui/forge_ui/static/stagectl.js +150 -0
  150. package/ui/forge_ui/static/style.css +1073 -0
  151. package/ui/forge_ui/static/theme.js +25 -0
  152. package/ui/forge_ui/transcripts.py +608 -0
@@ -0,0 +1,865 @@
1
+ # Optional Jev advisory judgments
2
+
3
+ Jev is TypeSafe's "System One" model: it answers narrow, typed questions (a
4
+ yes/no probability, a single choice, an ordered score) over supplied state in
5
+ about 100ms, with calibrated probabilities. Many questions per request evaluate
6
+ in parallel, so asking a few extra costs little additional latency. Forge uses
7
+ Jev to *advise*; Forge keeps every existing decision and gate. Jev never
8
+ replaces model routing logic, test selection, QA acceptance or repository
9
+ verification — it only offers a probability alongside them.
10
+
11
+ ## Privacy
12
+
13
+ Off by default, and this is the main reason why. Each enabled capability
14
+ transmits a narrow slice of state to an external API:
15
+
16
+ - **routing** — task prompts and candidate model identifiers
17
+ - **tests** — diff hunks and test names
18
+ - **gates** — diff hunks and file excerpts relevant to the gate
19
+ - **memory** — memory entry text being considered for curation
20
+
21
+ `.forge/` and `.git` are excluded from every payload, in every capability, with
22
+ no exception. The API key lives at
23
+ `${XDG_CONFIG_HOME:-$HOME/.config}/forge/jev.json`, mode `0600`. It is never
24
+ written to a run directory, a prompt, or a log.
25
+
26
+ A site mapped to the Cloudflare provider (see [Cloudflare Clef](#cloudflare-clef-a-second-provider))
27
+ sends the same slice to Cloudflare Workers AI instead of TypeSafe.
28
+
29
+ ## Setup
30
+
31
+ ```sh
32
+ ./forge jev setup # store an API key
33
+ ./forge jev status # resolved config, enabled capabilities
34
+ ./forge jev doctor [--live] # config/key checks; --live also probes the API
35
+ ./forge jev enable [--capability NAME] # persist enabled (optionally scoped)
36
+ ./forge jev disable [--capability NAME]
37
+ ```
38
+
39
+ `TYPESAFE_API_KEY` in the environment overrides the stored key for that
40
+ invocation only. Keys come from `console.typesafe.ai`.
41
+
42
+ A second provider, Cloudflare's hosted Clef, is set up with
43
+ `./forge jev setup --provider cloudflare --account-id <id>`; see
44
+ [Cloudflare Clef](#cloudflare-clef-a-second-provider).
45
+
46
+ ## Commands
47
+
48
+ User-facing:
49
+
50
+ ```
51
+ forge jev setup # store a key, validate it, enable Jev
52
+ forge jev status | doctor [--live] # what is on; whether it works
53
+ forge jev enable | disable [--capability routing|tests|gates|memory]
54
+ forge jev backtest --repo <path> [--capability tests|drift] [--execute]
55
+ ```
56
+
57
+ `score-plan`, `verify-discover` and `verify-triage` are also subcommands, but they are
58
+ called by `forge-parallel.sh` rather than by hand. They are documented here only so that
59
+ a line in a run log is traceable to the code that produced it.
60
+
61
+ ## Gating and precedence
62
+
63
+ Four steps, implemented identically by `scripts/forge-jev-options.sh` (shell)
64
+ and `scripts/forge_jev/` (Python) — keep them in sync:
65
+
66
+ 1. `FORGE_JEV=off` wins over everything: everything off, full stop. This is
67
+ the hard kill switch.
68
+ 2. Any `FORGE_JEV_<CAP>=on` acts as an allowlist — only those capabilities
69
+ run. `--jev-tests` alone works without `--jev`.
70
+ 3. Otherwise `FORGE_JEV=on` (from `--jev`) or the stored config's `enabled`
71
+ flag turns it on, and a capability explicitly set `off` is subtracted.
72
+ 4. Default: off.
73
+
74
+ Flags: `--jev` / `--no-jev` (mutually exclusive), `--jev-act`, `--jev-shadow`,
75
+ and `--jev-<cap>` / `--no-jev-<cap>` for `routing`, `tests`, `gates`, `memory`.
76
+ A run directory freezes its resolved selection in `jev-selection.json`, the
77
+ same way Fractal freezes `fractal-selection.json`, so resume and explicit
78
+ retry make the same decisions as the original run.
79
+
80
+ ## Advisory, always
81
+
82
+ Jev never selects a model, never changes a task's tier, never overturns a
83
+ `FORGE_VERDICT`, and never marks anything verified. `--jev-shadow` records
84
+ judgments and changes nothing at all.
85
+
86
+ `--jev-act` still parses, so old commands and a frozen `jev-selection.json` keep
87
+ working, and it still marks each judgment `acting` in `jev.jsonl` and the run
88
+ dataset. Nothing reads it as permission: the one thing it unlocked, writing Jev's
89
+ tier into the `difficulty` column, was removed on 2026-10-17 (see Routing).
90
+
91
+ ## Fail-open
92
+
93
+ Any error, timeout, or missing key is logged as a skip reason and the run
94
+ proceeds exactly as it would without Jev. A Jev network call can never fail a
95
+ run.
96
+
97
+ ## Observability
98
+
99
+ - `<run-dir>/jev.jsonl` — one row per request: site, question ids, answers,
100
+ confidences, latency, tokens, and whether the judgment was applied.
101
+ - `<run-dir>/jev.skip` — present when a request was skipped, with the reason.
102
+
103
+ ## Backtest
104
+
105
+ `forge jev backtest` measures whether Jev's `test_relevance` rubric (see
106
+ `scripts/forge_jev/questions.py`) picks the right tests for a change, before
107
+ that judgment is ever wired into a live decision.
108
+
109
+ The label comes from git history, not from a model: a commit where a human
110
+ changed source and test files together is treated as ground truth — "these
111
+ are the tests a person judged relevant." Non-circular because nothing Jev (or
112
+ any model) produced enters the label. `scripts/forge_jev/corpus.py` extracts
113
+ these (change, tests-touched) pairs from `git log`.
114
+
115
+ Candidates for a commit are the test files present in its **parent** tree, not
116
+ at HEAD. Scoring against HEAD would ask about tests that did not exist yet:
117
+ measured on one repo, a commit 300 back had 94 test files in tree against 416
118
+ at HEAD, which would inflate the suite-reduction metric more than fourfold.
119
+
120
+ ```sh
121
+ ./forge jev backtest --repo PATH --limit 50 # dry estimate, no API call
122
+ ./forge jev backtest --repo PATH --execute --max-requests 200
123
+ ./forge jev backtest --repo PATH --execute --capability drift --json
124
+ ```
125
+
126
+ `--execute` is required before backtest makes a single API call; without it,
127
+ the command only estimates the run (commit count, request count) and says so.
128
+ `--max-requests` caps spend by bounding how many Jev requests the run is
129
+ allowed to make, regardless of `--limit`. Results land under `--out` (default:
130
+ a directory under the run's own temp/output location) as raw judgments plus
131
+ the printed summary — precision/recall of the rubric's "true" answers against
132
+ the commit's actual test changes.
133
+
134
+ The summary sweeps thresholds and reports micro/macro recall, precision,
135
+ suite reduction, and `records_with_full_recall` — the fraction of commits where
136
+ *every* labeled test was selected. That last one is the number that decides
137
+ whether the judgment is safe to act on: a selector averaging 95% recall by
138
+ missing one test in every commit is useless. For `--capability drift` the
139
+ headline is instead precision at the warn threshold, because a warning that
140
+ fires on files nobody edits trains people to ignore it.
141
+
142
+ Three honesty caveats, printed with every run so they travel with the numbers:
143
+
144
+ - Commit messages are written **after** the work and often name the very files
145
+ or behaviour that changed; a Forge task prompt is written before. Backtest
146
+ scores are therefore an **upper bound** on live performance.
147
+ - A commit's test changes are a **lower bound** on the tests that were
148
+ actually relevant — humans miss tests too. High recall against this label
149
+ is necessary to trust the rubric, but not sufficient; it cannot prove the
150
+ rubric would catch tests no human thought to touch. (This one does not apply
151
+ to `--capability drift`, whose label is the commit's complete file set.)
152
+ - Co-change encodes **one team's testing habits and repo layout**, not a
153
+ universal property of the change. A number measured on one repo does not
154
+ transfer to another's thresholds without re-measuring there. Measured here:
155
+ labeled tests are 1.6% of the candidate suite in one repo and 13.2% in
156
+ another, which are not the same problem.
157
+
158
+ ## Verification (the first live capability)
159
+
160
+ Two judgments inside `verify_result`, the decomposed runner's verification step.
161
+ Both are off unless the `tests` capability is enabled and a key is configured; with
162
+ Jev inactive the function behaves exactly as it did before.
163
+
164
+ **Finding a command.** Only when neither `--verify` nor `.forge/verify` exists — the
165
+ case that reports `UNVERIFIED` today even when QA passed. Code enumerates commands
166
+ that *demonstrably exist* in the repo (`package.json` scripts, Makefile targets,
167
+ tox/nox, runner scripts under `tests/`, `scripts/`, `bin/`, a configured pytest,
168
+ `go.mod`, `Cargo.toml`, `run:` steps in GitHub workflows, and facts recorded under the
169
+ `## Verify` heading of `.forge/memory.md`); Jev picks one, or `none`. Each candidate is
170
+ sent with its source and an excerpt of what it actually runs — a make target's recipe
171
+ rather than the head of the Makefile — because the command name alone does not say
172
+ whether it runs the suite. It selects, never generates, so it cannot propose `make test`
173
+ for a repo with no Makefile. A non-executable runner is
174
+ offered with its interpreter — forge's own `tests/check.sh` is mode 644 and its README
175
+ says `bash tests/check.sh`.
176
+
177
+ The chosen command is announced through `note` before it runs, recorded with its
178
+ confidence and evidence in `<out>/verification.jev.json`, and then flows through the
179
+ existing run/fingerprint/status logic unchanged. `UNVERIFIED` remains the outcome
180
+ whenever nothing is chosen: Jev may find a command, never declare something verified.
181
+
182
+ **Re-running a corroborated flake.** On a non-zero exit, Jev classifies the failure as
183
+ `real_regression`, `known_flake`, `environment` or `generated_drift`. The command is
184
+ re-run **once**, and only when all of these hold:
185
+
186
+ - the exit was non-zero *and* the tree did not change — a tree change is never a flake;
187
+ - the verdict is `known_flake` at or above the `flake_act` threshold (0.90 by default);
188
+ - a `trap` already recorded in `.forge/memory.md` shares a distinctive token with this
189
+ failure's output.
190
+
191
+ That last condition is deterministic and computed in code, not asked of the model. A
192
+ model alone calling a failure a flake is how a real regression ships, and a trap noted
193
+ months ago must not excuse an unrelated failure today. If the re-run also fails the
194
+ status stays `FAIL` and the first log is preserved. `real_regression` is never re-run
195
+ at any confidence.
196
+
197
+ **Interaction with the deterministic rerun.** `forge-parallel.sh` now reruns *any* failed
198
+ verify command once by default (`--verify-retries`, default `1`) when the tree did not
199
+ change, with no model and no recorded trap needed: the rerun itself is the evidence, and a pass
200
+ is reported loudly as `FLAKY` (`verification.flaky`) rather than as a clean pass. That rerun
201
+ comes first, so Jev's triage above only runs when the deterministic one is disabled
202
+ (`--verify-retries 0`) — in which case the corroboration requirements above still apply
203
+ unchanged. A deterministic rerun that also fails ends in `FAIL` without consulting Jev.
204
+
205
+ ## Calibration (measured 2026-09-18)
206
+
207
+ First measurements against the live API. Two repos for test selection, 18 for verify
208
+ discovery. Every number below is from this machine's repos and does not transfer —
209
+ re-measure before trusting a threshold elsewhere.
210
+
211
+ ### `tests_act` = 0.70 is supported by evidence, not a guess
212
+
213
+ Discovery was run over every git repo on this machine (18 with candidates). The pick
214
+ was compared against the command each repo's own README or `package.json` documents.
215
+
216
+ | | |
217
+ |---|---|
218
+ | Fires at 0.70 | 10 of 18, **every pick correct** |
219
+ | Lowest correct pick that fires | 0.73 (`ctrlrisk-tools-redigo`) |
220
+ | Highest *incorrect* pick | 0.55 (`spankai` → `cargo test`) |
221
+ | Margin around the threshold | **0.18** |
222
+
223
+ `spankai` is the one confirmed wrong pick: a Rust + TypeScript monorepo whose README
224
+ says `npm run check`, where Jev chose `cargo test` — a real test command that covers
225
+ only the Rust half. The gate blocked it. Lowering the threshold to 0.58 would add four
226
+ correct picks but leave only a 0.03 margin above that wrong one, so 0.70 stays.
227
+
228
+ Confidence tracks how ambiguous the candidate set is, not how likely the pick is to be
229
+ wrong: repos with 2 candidates land at 0.73–0.81, repos with 11–25 land at 0.58–0.61.
230
+ This is why the gate was not `runs_tests`, which sat at 0.79–0.98 for every repo
231
+ including the wrong `cargo test` at 0.97. `runs_tests` answers "is this a test command" —
232
+ true of `cargo test` — and is nearly useless as a gate.
233
+
234
+ Repeating the identical request three times gives a spread of 0.02–0.08 and never
235
+ changes the pick, so these numbers are reproducible rather than one lucky sample.
236
+
237
+ #### The gate is no longer Choice confidence, because that answers the wrong question
238
+
239
+ Confidence measures *which* option wins. For a selection among several **acceptable**
240
+ options that is the wrong quantity: two commands that both run the suite split the mass
241
+ and depress it, and a tie is not doubt. Measured live on the jevscope repo, three times
242
+ running — `bash scripts/check.sh` and `make test` both run its suite —
243
+
244
+ | | |
245
+ |---|---|
246
+ | Choice confidence | **0.48–0.52**, against a 0.70 gate |
247
+ | Mass on `none` | **0.01–0.02** |
248
+ | Actual state of the repo | 48 tests, all passing |
249
+
250
+ So forge reported `UNVERIFIED` for a verifiable repo while the model was 98% sure some
251
+ listed command verified it. The gate now reads **1 − P(none)** — the probability that the
252
+ repo is verifiable at all, which is what `tests_act` was always meant to bound. A split
253
+ between correct commands leaves it high; genuine doubt piles onto `none` and drops it,
254
+ and the winner is then just the tie-break. `client.probabilities` exposes the
255
+ distribution the response was already returning and `choice()` was discarding. When a
256
+ response omits it, the old confidence gate stands — a missing field is not certainty.
257
+
258
+ #### …and 0.70 itself is now stale, for either gate
259
+
260
+ `spankai` is the one confirmed wrong pick in the table above: `cargo test` covers only
261
+ the Rust half of a Rust + TypeScript monorepo, and at 0.55 the 0.70 gate blocked it.
262
+ **It no longer does.** Re-run today it scores 0.77–0.81, which clears 0.70 on the old
263
+ gate as well as the new one.
264
+
265
+ The cause is measurable rather than guessed. Suppressing `_evidence_excerpt` — restoring
266
+ the state as it was when 0.55 was recorded — reproduces 0.52–0.59; restoring it gives
267
+ 0.77–0.81. Sending a `make` target's recipe raised confidence across the board, and it
268
+ raised the wrong pick with everything else.
269
+
270
+ So the whole table above was measured against a state production no longer produces, for
271
+ the seventh time in this file. `tests_act` needs re-measuring across those 18 repos in
272
+ the current state before 0.70 means anything, and **no number should be moved until it
273
+ is** — fitting a new one to the single data point above is exactly the mistake being
274
+ recorded here.
275
+
276
+ ### Test selection: no single threshold works across repos
277
+
278
+ `backtest --capability tests` over 25 commits of `ctrlrisk-tools-enrich` (40 requests)
279
+ and all 44 of `spankai` (44 requests), sweeping the per-file relevance probability:
280
+
281
+ | repo | highest threshold holding recall ≥ 0.95 | suite reduction there |
282
+ |---|---|---|
283
+ | `ctrlrisk-tools-enrich` | 0.20 | 46% |
284
+ | `spankai` | 0.45 | 36% |
285
+
286
+ The safe threshold differs by 2.25× between two repos, which settles the question of
287
+ whether one global default could serve: it cannot. Any future test-subsetting capability
288
+ must calibrate per repo and store the result, not ship a constant. Note also that no
289
+ answer ever exceeded 0.90 — the top of the probability range is unused by this rubric,
290
+ so a threshold set there silently disables the capability.
291
+
292
+ ### A rejected change, recorded so it is not retried
293
+
294
+ Feeding each repo's documented commands (README, CONTRIBUTING, AGENTS.md) into
295
+ discovery state looked obviously right and **measured worse**. It fixed `spankai`'s pick
296
+ but cut repos firing at 0.70 from 10 to 3, and it destabilised the judgment: against a
297
+ baseline spread of 0.02–0.08, `ctrlrisk-intelligence` fell from a steady 0.81/0.81/0.83
298
+ on the correct command to 0.30 on a narrow migration check. A README lists many
299
+ commands and nothing marks which one is the gate, so the evidence is mostly noise.
300
+ Half the damage was a prompt artifact — naming the state key when the list was empty
301
+ cost 0.12–0.21 confidence on every repo that documents nothing, because "no candidate
302
+ matches" reads as evidence against rather than absence of evidence. Fixing that
303
+ recovered those repos exactly, and the doc-rich repos stayed erratic. Reverted.
304
+
305
+ ## Routing (shadow-first)
306
+
307
+ Five `Score` rubrics per task, all in one request, evaluated in parallel. **This code,
308
+ not the model, turns them into a tier** — `forge_jev/routing.py` holds the weights, so
309
+ re-tuning them against accrued outcomes replays recorded scores instead of paying for
310
+ inference again.
311
+
312
+ | Rubric | Weight | Why |
313
+ |---|---|---|
314
+ | design judgment | 0.35 | `decompose.md` defines difficulty by this first |
315
+ | state & concurrency subtlety | 0.25 | that table names it as what makes a ten-line diff `high` |
316
+ | blast radius | 0.20 | how far a mistake reaches |
317
+ | spec clarity | 0.15 | is the approach given, or must it be invented |
318
+ | existing test coverage | **−0.05** | inverted: good coverage lowers the tier |
319
+
320
+ Coverage is weighted lightly on purpose. It does not reduce the judgment a task demands,
321
+ only what a mistake costs, and letting it dominate would send a genuinely hard change to
322
+ a cheap model because the area happens to be well tested.
323
+
324
+ The planner's own difficulty rating is **not** sent. Jev is a second opinion on the same
325
+ evidence; showing it the answer first buys an agreement rate, not a judgment.
326
+
327
+ **Escalation.** A composite within 0.05 of a tier boundary, or a confidence below 0.55,
328
+ escalates one tier and records why (`boundary` / `low-confidence`). Paying for a stronger
329
+ model on an ambiguous task is the cheap error.
330
+
331
+ ### Two modes, and why the tier is never written
332
+
333
+ | Mode | What happens |
334
+ |---|---|
335
+ | `--jev-shadow` | Scores every task, writes `jev-routing.tsv` and `tasks/<id>/jev.json`, **changes nothing else** — not the table, not one dispatch |
336
+ | `--jev` (default) | Adds an advisory line per task; the `difficulty` column is untouched |
337
+
338
+ A third mode, `--jev-act`, could write the tier into `difficulty` once a repo held a
339
+ calibration for that tier (`forge jev calibrate --write`: 30 tasks, a 95% Wilson lower
340
+ bound over 0.80). No repo ever reached it, so it never acted, and on 2026-10-17 it was
341
+ removed along with `calibrate` and the `routing_act` and `calibrate_floor` thresholds.
342
+
343
+ The run dataset had settled the question by then. Jev's score predicts a task's first
344
+ pass about as well as cheap features do (AUC 0.69 against 0.64–0.67 on older runs; on the
345
+ newest runs every method, Jev included, sat near chance), and a model-swap test on Clef
346
+ showed a decision model judging how hard a task is, not which model does it better:
347
+ swapping the model explained 2% of the variance in its predictions. A tier is a choice of model, so it is measured instead: exploratory routing,
348
+ on by default, and `forge dataset routing` compare models per tier from randomized
349
+ assignments (see `decompose.md`, Exploratory routing).
350
+
351
+ ### What was measured
352
+
353
+ Six real tasks from this repo, hand-labelled first, scored live:
354
+
355
+ - **5 of 6 matched.** The sixth was a boundary escalation `medium`→`high`, the intended
356
+ conservative direction.
357
+ - Confidence is high on easy work (0.89–0.93) and lower on hard work (0.67–0.76). That
358
+ asymmetry is the useful one: Jev is confident exactly where routing *down* saves money,
359
+ and defers to the human where getting it wrong is expensive.
360
+ - End to end, a spelling fix declared `high` was routed to the cheap pool at 0.864 while a
361
+ concurrency redesign stayed on the expensive one.
362
+
363
+ Composite confidence is the **weight-weighted** mean, not the minimum. The minimum was
364
+ tried first and measured wrong: on a real task `blast_radius` returned 0.0 while
365
+ `design_judgment` (weight 0.35) was 0.94, and the minimum reported 0.0 for a composite
366
+ that was mostly settled. The weakest dimension is still reported separately as `weakest`.
367
+
368
+ Six hand-labelled tasks is **not** a calibration. It is a smoke test that the rubrics
369
+ point the right way.
370
+
371
+ ## Pre-dispatch gates
372
+
373
+ Three plan-time warnings. All advisory, all fail-open, none can block a dispatch — a gate
374
+ that can stop a dispatch is a gate that will eventually stop a good one.
375
+
376
+ **Prompt adequacy** and **independent verifiability** ride inside the routing request.
377
+ They need exactly the state routing already sends, and questions in one request evaluate
378
+ in parallel, so they cost **no extra round trip**.
379
+
380
+ **`files` drift prediction** needs a different universe — every tracked file, not the task
381
+ text — so it is the one extra request Phase 4 adds, and only when the `gates` capability
382
+ is on. `decompose.md` calls the `files` column "a promise, not a prediction"; Forge
383
+ already detects a broken promise in `drift.txt`, but only after the wave has run. Asking
384
+ at plan time turns a post-mortem into a warning while the plan can still change.
385
+
386
+ ### Each rubric has its own threshold, because they are not on one scale
387
+
388
+ This is the same trap `tests_act` set earlier, and it was measured rather than guessed.
389
+
390
+ | Gate | Threshold | Evidence |
391
+ |---|---|---|
392
+ | drift | `gate_warn` 0.60 | 50 commits × 2 repos: precision **1.000** and **0.934** |
393
+ | prompt adequacy | `prompt_warn` 0.55 | unjudgeable prompts **0.06–0.27**, prompts stating checkable behaviour **0.85–0.96** |
394
+ | independent verifiability | `verifiable_warn` 0.55 | fragments **0.26–0.65**, self-contained tasks **0.65–0.96** — precision gate, see below |
395
+
396
+ Both text gates were first measured at a time when `prompt.md` did not reach the model:
397
+ `score_task` sent only the task title. Those numbers (vague 0.04, specific 0.55–0.87)
398
+ described data production never produced, and both thresholds were fitted to them.
399
+
400
+ Re-measured with the real state — three runs over eight probe prompts against a real
401
+ repo, plus the eight prompts of a real plan:
402
+
403
+ **`prompt_adequacy` separates cleanly.** "Make the error messages better" scores 0.06;
404
+ "harden the server — fix what you find" 0.15; "make it faster without breaking anything"
405
+ 0.27. A prompt that states checkable behaviour scores 0.85–0.96, and so did every one of
406
+ the eight real task prompts. A 0.58-wide gap, so the threshold sits in the middle of it
407
+ with 0.28 clear on each side. The old 0.30 classified every case correctly too, but sat
408
+ **0.03** above the worst true positive — correct by luck rather than by margin, against a
409
+ measured repeat-noise of 0.07.
410
+
411
+ **`independently_verifiable` was unanswerable, and rewording it was never going to
412
+ help.** The rubric asked whether confirming a task requires "work declared in a different
413
+ task" — and `score_task` sent no other task. The model was asked about a plan it had
414
+ never been shown, so it could only guess whether such a sibling existed. That is why the
415
+ classes overlapped (0.34–0.56 against 0.43–0.90), and it is the same failure as
416
+ `prompt.md`: a rubric scored against evidence that never reached it.
417
+
418
+ The fix is state, not wording. `routing._siblings` now sends every other task in the plan
419
+ — id, title, file scope, declared deps, capped at `MAX_SIBLINGS` — plus the task's own
420
+ `deps`, and the rubric points at that list by name.
421
+
422
+ Re-measured on the eight tasks of the real jevscope plan, eight repeats on every task the
423
+ threshold depends on. Ground truth was taken from the **import graph and wave position**,
424
+ not from the task titles — which corrected the labelling, not the model:
425
+
426
+ | task | score | truth |
427
+ |---|---|---|
428
+ | `server` | 0.26–0.34 | fragment: wave 1, imports `discover`/`events`/`stats` and serves `page`'s `index.html`, **none declared** |
429
+ | `page` | 0.43–0.53 | fragment: consumes `server`'s API; both defects that shipped were `page`↔`events` shape mismatches |
430
+ | `readme` | 0.60–0.65 | fragment: documents the wave-2 CLI, and shipped wrong |
431
+ | `cli` | 0.65–0.75 | verifiable: wave 2, so everything it needs has already landed |
432
+ | `stats`, `events`, `discover`, `fixtures` | 0.67–0.96 | verifiable: own file set, own test file |
433
+
434
+ **At the old 0.38 this gate caught nothing on a real plan** — every genuine fragment
435
+ scored above it — exactly as wave coupling had never fired. 0.55 catches `server` with
436
+ 0.21 to spare and `page` with 0.02, and keeps **0.10** to the nearest true negative,
437
+ which is the only margin here that beats the 0.07 noise floor. An advisory gate has to
438
+ hold precision above all: one that cries wolf is one people switch off.
439
+
440
+ The classes still touch at 0.65, where `readme` meets `cli`. So this is a precision gate
441
+ that buys a clean left-hand side by giving up `readme` entirely, and **silence above 0.55
442
+ still means "no evidence", not "verifiable"**.
443
+
444
+ Using the single `gate_warn` of 0.60 for all three — which is what shipped first — warned
445
+ on 5 of 9 and 5 of 11 perfectly good tasks in the hand-labelled set. It was caught by a
446
+ live plan where a precisely specified task tripped both text gates. Precision is the only
447
+ number that matters for a warning: one that fires on good work teaches people to ignore
448
+ every future warning.
449
+
450
+ Drift is the strongest of the three and the only one measured at corpus scale. Its
451
+ threshold sits comfortably clear of the noise on real repos (non-edited files scored
452
+ ≤0.23 in a live check where the true file scored 0.65), but note that 0.65 is not a wide
453
+ margin — a borderline file can fall either side between runs.
454
+
455
+ The verifiability rubric was nearly discarded on a first reading that showed it failing
456
+ to separate vague prompts from clear ones. That was a bad label, not a bad rubric:
457
+ vagueness is not the same property as needing a sibling task to land first. Against the
458
+ right labels it separates with a 0.27 margin.
459
+
460
+ ## Post-review annotation
461
+
462
+ When a reviewer returns `FORGE_VERDICT: FAIL`, Jev reads the review against the diff and
463
+ says whether the failure looks sound. **It cannot change the verdict, and no caller is
464
+ given a way to.** The status file is written before this runs and nothing here rewrites
465
+ it; a test asserts that structurally, by reading the shipped block and failing if a
466
+ status write ever appears inside it.
467
+
468
+ That restraint is the point. Forge's second invariant is reviewer independence, and a
469
+ model that could talk a reviewer out of a FAIL would produce confident PASSes on code
470
+ nobody checked — worse than having no reviewer. The asymmetry runs the other way too:
471
+ nothing ever annotates a PASS into looking suspect, because a PASS is the outcome that
472
+ gets merged and its reviewer's judgement is left alone.
473
+
474
+ Two questions, one request:
475
+
476
+ | | |
477
+ |---|---|
478
+ | **correctness** | does the review name a real bug, or only preferences about how the code is written? The QA prompt already says "style nits are not failures" |
479
+ | **citation** | do the files, lines and quoted code the review cites actually appear in this diff? |
480
+
481
+ Measured on three hand-written reviews against one diff:
482
+
483
+ | review | correctness | citation | flagged |
484
+ |---|---|---|---|
485
+ | style nits only ("`i` should be `item`") | 0.21 | 0.80 | yes — style |
486
+ | a real bug (discount below zero, with a triggering input) | 0.96 | 0.88 | no |
487
+ | cites `src/payment.py`, absent from the diff | 0.45 | 0.02 | yes — citation |
488
+
489
+ A low citation score suppresses the correctness reason rather than printing both. The
490
+ hallucinated review scored 0.45 on correctness and would have been reported as "style
491
+ preferences", which describes the wrong problem: nothing can be said about the
492
+ correctness of code that is not there.
493
+
494
+ ## Wave coupling
495
+
496
+ `compute_waves` bounds parallelism structurally: two tasks share a wave only when their
497
+ declared file sets are disjoint. Disjointness cannot see the case where task A changes an
498
+ interface task B consumes — both diffs are individually correct, both pass review against
499
+ their own baseline, and the merge is broken. This asks about exactly that.
500
+
501
+ **It never fired once, on any plan, until it was measured.** It borrowed `gate_warn`
502
+ (0.60) while coupling probabilities live around 0.09–0.38. That was not a strict
503
+ threshold; it was an unreachable one.
504
+
505
+ Measured over 11 same-wave pairs from a real 8-task plan, five runs each, with ground
506
+ truth taken from the run itself — two of those pairs produced a defect that actually
507
+ shipped:
508
+
509
+ | | median |
510
+ |---|---|
511
+ | `events`+`fixtures` — fixtures reads an event's answers object | **0.27** |
512
+ | `events`+`page` — page renders normalised events | **0.25** |
513
+ | `page`+`stats` — page rendered a repeats shape stats never emitted *(shipped bug)* | **0.23** |
514
+ | `events`+`stats` — stats consumes `normalise()`'s output shape | **0.22** |
515
+ | `discover`+`readme` — unrelated | 0.18 |
516
+ | `discover`+`server`, `readme`+`fixtures` — unrelated | 0.13–0.14 |
517
+ | `discover`+`fixtures`, `fixtures`+`page`, `discover`+`page` — unrelated | 0.09–0.10 |
518
+
519
+ Two changes came out of it.
520
+
521
+ **The requirements now reach the rubric.** It used to see `id (title) files: …` and
522
+ nothing else, and in that state it had *no signal at all*: the pairs that produced real
523
+ defects scored lowest of everything, below pairs that were entirely unrelated. An
524
+ interface dependency between two tasks is written down in `prompt.md` and nowhere else —
525
+ certainly not in a one-line title. File *contents* are still not sent; diffs nobody has
526
+ written yet would be noise.
527
+
528
+ **`coupling_warn` is 0.20, and at most three pairs are reported.** The threshold decides
529
+ whether anything is worth saying; the ordering decides what gets said. Rank is the real
530
+ signal — the coupled pairs took four of the top five places — while the absolute scores
531
+ move with the plan: the same rubric sat at 0.09–0.27 in the measurement and 0.20–0.38 on
532
+ the live plan, where a bare 0.20 warned about 12 of 21 pairs. A gate that fires on more
533
+ than half of everything teaches people to ignore it.
534
+
535
+ On the live plan it now names three pairs, all genuinely coupled, `page`+`server` highest
536
+ — the page consumes the server's API contract. On a plan of eight unrelated tasks it says
537
+ nothing.
538
+
539
+ ### The ordering half: `forward_dependency`
540
+
541
+ The other shipped defect was `readme` documenting a CLI interface that `cli` had not
542
+ built yet. Those two were in *different waves*, so they were never a candidate pair, and
543
+ no same-wave check can see that shape. It is a different failure from coupling, too:
544
+ not a broken merge between concurrent tasks, but a task reviewed, verified and merged
545
+ **before the thing it was written against exists** — so its own review cannot catch it,
546
+ and the error ships.
547
+
548
+ `forward_pairs` forms ordered `(earlier, later)` pairs across waves, skipping any pair
549
+ where LATER is named in EARLIER's declared deps — forge would not have placed EARLIER
550
+ first in that case, so the risk cannot arise. Cross-wave pairs get their own
551
+ `MAX_FORWARD_PAIRS` budget rather than sharing `MAX_PAIRS`, or one large first wave would
552
+ spend the whole allowance inside itself and never ask the ordering question at all.
553
+
554
+ Measured over that plan's seven cross-wave pairs, five runs each:
555
+
556
+ | pair | score | |
557
+ |---|---|---|
558
+ | `readme` → `cli` | **0.88–0.90** | documents the CLI built in the next wave *(shipped bug)* |
559
+ | `page` → `cli` | 0.13–0.15 | |
560
+ | the other five | 0.05–0.07 | |
561
+
562
+ A **0.73-wide gap**, the cleanest separation any rubric here has produced, so
563
+ `forward_warn` sits in the middle of it at 0.50 — 0.35 clear below, 0.38 above, against a
564
+ 0.07 noise floor. Provisional in one specific way: the gap is enormous but rests on a
565
+ single positive example. A second plan could move the positive class; it is unlikely to
566
+ move it 0.38.
567
+
568
+ Both halves run from one `forge jev coupling` invocation, so a plan gets both warnings or
569
+ neither, and each row carries a trailing `kind` of `same-wave` or `forward`. On the real
570
+ plan the gate now names all three of its genuine problems: `page`+`server` and
571
+ `events`+`page` same-wave, and `readme`→`cli` at 0.88.
572
+
573
+ Still advisory in every mode: it prints, and never reorders a wave or blocks a dispatch.
574
+
575
+ ## Memory curation
576
+
577
+ Guarded so `--no-memory`, `FORGE_MEMORY=off` and a disabled capability each yield today's
578
+ exact behaviour, verified against a pristine `HEAD` export.
579
+
580
+ The asymmetry that shapes every default here: `memory.md` is injected into every dispatch
581
+ of every future run, so a wrong fact is paid for forever, while `ledger.tsv` is
582
+ append-only and never injected. **Nothing recorded is ever lost — only injection is
583
+ gated.** A rejected line still lands in the ledger in full.
584
+
585
+ | At `record` | |
586
+ |---|---|
587
+ | **quality gate** | a one-off observation is recorded but not injected |
588
+ | **category correction** | refiles a fact under `verify`/`trap`/`finding`; `none` leaves it alone |
589
+ | **semantic dedup** | merges a paraphrase onto the existing key |
590
+
591
+ Dedup fixes a limitation `norm_key`'s own comment names: exact-match keys mean two runs
592
+ learning the same thing in different words count as two facts, so neither reaches the
593
+ two-distinct-runs promotion threshold for a `finding`. Live, four learnings produced:
594
+ a rename note recorded but not injected (durable 0.1); a `trap` refiled to `verify`; a
595
+ differently-worded restatement of it merged onto the same key; and a genuine new trap
596
+ kept.
597
+
598
+ **Category correction and dedup are two rubrics, and they have two thresholds.** They
599
+ shared one `memory_dedup` value at 0.80 until both were measured. Splitting them was
600
+ right; the first numbers put on them were not, and the way that went wrong is the useful
601
+ part.
602
+
603
+ The first pass fed existing entries as `verify | text` and read correct merges at
604
+ 0.99–1.00, which set the bar at 0.90. Running the real CLI against a real `memory.md`
605
+ then returned **0.90 on a correct merge** — right on the threshold. Production strips
606
+ entries to bare text under a `## section` heading, with no category prefix; the state
607
+ was different, so the number was. Re-measured with the state
608
+ `cmd_memory_curate` actually builds, six repeats per boundary case:
609
+
610
+ | | wrong, must not act | right, must act | threshold |
611
+ |---|---|---|---|
612
+ | `memory_merge` | 0.56–0.72 | 0.91–1.00 | **0.85** |
613
+ | `memory_recategorize` | 0.56–0.72 | 0.92–1.00 | **0.80** |
614
+
615
+ Both rubrics separate in the same place. What sets the two numbers apart is therefore not
616
+ the scale but the cost: a wrong merge loses a fact permanently, so it takes the top of the
617
+ gap, while a wrong refile leaves the fact in place under the wrong heading and refusing a
618
+ correction is the commoner harm, so it takes the bottom. **0.80 was a defensible value for
619
+ one of the two all along** — the fault was never measuring it, and the first measurement
620
+ was worse than the guess.
621
+
622
+ The wrong merges are worth naming, because the threshold is the only thing that stops
623
+ them: `config/prod.yaml` is ciphertext / `config/prod.yaml` must not hold plaintext
624
+ secrets merged on 6 of 6 runs at 0.56–0.72. The rubric's `none` criterion already tells
625
+ the model to keep both on a partial match and it picks one anyway — at a confidence that
626
+ separates cleanly from a true paraphrase. That is the case for gating on confidence
627
+ rather than wording the criterion harder.
628
+
629
+ The refused corrections are facts that genuinely span two categories: a test that fails
630
+ without `alembic upgrade head`, and `make test` regenerating fixtures so a dirty tree is
631
+ expected. Both are verify and trap at once, which is exactly when the recorded category
632
+ should stand.
633
+
634
+ **A merged line is exempt from the quality gate.** The two judgments arrive in one
635
+ request and originally did not consult each other, which broke dedup in the case dedup
636
+ exists for. `rebuild` skips a non-injectable row *before* it counts anything, and a
637
+ `finding` needs two distinct runs to be promoted — so a restatement judged not durable
638
+ was dropped from the count of the fact it restates, and the two wordings still never
639
+ reached two.
640
+
641
+ Measured over paraphrases of facts already in memory: durability 0.44–0.83, against
642
+ 0.82–0.83 for the same facts stated fresh. A restatement reads as less durable than the
643
+ thing it restates. That is the wrong question to ask of it: the entry it matched is
644
+ already in `memory.md`, so durability was settled when *that* entry was written, and a
645
+ line matching it is by definition not the one-off observation the gate is for. The
646
+ exemption is the merge itself — a match refused by `memory_merge` grants nothing.
647
+
648
+ The ledger gained an eleventh column for the injectable flag. `rebuild` treats a row
649
+ without it as injectable, so an existing ledger keeps behaving as it did.
650
+
651
+ | Elsewhere | |
652
+ |---|---|
653
+ | **semantic staleness** (`prune` only) | an entry whose anchor still exists but whose fact went false |
654
+ | **injection slicing** (`inject`) | narrows the role slice to the ~8 facts bearing on this task |
655
+
656
+ Staleness runs **only** on an explicit `forge-memory.sh prune`, never during `record`.
657
+ `rebuild` runs after every dispatch, so a request per entry per dispatch would be the
658
+ most expensive thing in a run.
659
+
660
+ Its threshold is `stale_drop` at 0.25, far below `gate_warn`. Measured over six facts
661
+ against real files: ones that had genuinely gone false scored 0.03–0.10, ones still true
662
+ scored 0.47–0.96. `gate_warn`'s 0.60 sits inside the true range and would have deleted a
663
+ fact the file still supports. Dropping a fact is irreversible and silent; keeping a stale
664
+ one costs a line until someone notices.
665
+
666
+ Slicing only removes lines — never adds, never reorders, and keeps the section heading
667
+ above each kept line. The 40-line/4KB cap and the role slice remain the outer bound. Live
668
+ on a 10-fact memory against a payments-retry task, it kept 8 and dropped the CSS build
669
+ ordering and the parser cache — while keeping the idempotency trap.
670
+
671
+ ## Removed: effort sizing, spec parsing, Fractal re-rating
672
+
673
+ All three were removed on 2026-10-17, and none had ever acted.
674
+
675
+ - **Effort sizing** rode in the routing request and suggested a `low..ultra` level per
676
+ task, with QA effort sizing (`qa-effort`) doing the same for the reviewer. Over the run
677
+ dataset the dwarf suggestion separated first-pass outcomes at AUC 0.64 overall but
678
+ 0.50–0.54 within a tier: all it knew was the tier. And 81% of its suggestions were
679
+ `xhigh` or higher. Effort is now measured like models are: pools that differ only in
680
+ effort (`--dwarf-low sol --dwarf-medium sol:high --dwarf-high sol:xhigh`) give
681
+ exploratory routing two arms per tier, and `forge dataset routing` reports whether the
682
+ higher effort passes more.
683
+ - **`forge jev parse-spec`** mapped a sentence onto `--dwarf-<tier>` flags. It could only
684
+ map a sentence that already named the aliases, since `registry.tsv` says nothing about
685
+ cost or strength, which made it no shorter than typing the flags.
686
+ - **Fractal child re-rating** could move a child between authorised pools, gated on the
687
+ same per-repo calibration as `--jev-act`, which no repo had. A child's difficulty is the
688
+ parent dwarf's own again.
689
+
690
+ ## Early test feedback (what became of per-task subsetting)
691
+
692
+ Running tests inside a task worktree has no safe window: an artifact created before the
693
+ diff is captured reaches QA as if the dwarf wrote it, and one created after trips the
694
+ fingerprint re-checks in `do_task` and `merge_task`, which write `INVALIDATED` and block
695
+ the merge. So this exports the reviewed commit to a throwaway directory and runs there.
696
+ A test asserts the worktree's tree hash, file list and absence of `__pycache__` across a
697
+ real run.
698
+
699
+ **It is not a speed-up.** The full suite still runs at integration exactly as before, so
700
+ this *adds* a run rather than replacing one. The saving the plan described required
701
+ replacing verification, which the fingerprint rules forbid. What it buys instead is that
702
+ a reviewer learns "your diff breaks `test_x`" while it can still act on that.
703
+
704
+ ### It is opt-in, and needs a template
705
+
706
+ `.forge/verify-subset` must exist and contain `{files}`:
707
+
708
+ ```
709
+ pytest -q {files}
710
+ npm test -- {files}
711
+ python3 -m unittest {files}
712
+ ```
713
+
714
+ Deliberately a separate file from `.forge/verify`. There is no general way to restrict an
715
+ arbitrary command to a file list, and the first version assumed there was — it appended
716
+ paths to the verify command, which works for `pytest -q` and fails for `npm test`,
717
+ `go test ./...`, `bash tests/check.sh` and `python -m unittest discover`, each for a
718
+ different reason. Without the template this feature stays off.
719
+
720
+ ### A failure is only reported when the base passes
721
+
722
+ The first run of this reported **"the likely-affected tests FAILED"** when the real
723
+ problem was that `pytest` was not installed in the export. An environment failure
724
+ presented to a reviewer as a broken diff is worse than silence, and pre-existing failures
725
+ have the same shape.
726
+
727
+ So on a failure the identical subset runs again against the commit the task started from,
728
+ and the result is reported **only if the base passes**. That one extra run — paid only
729
+ when something already failed — is what separates "this diff broke it" from "this was
730
+ already broken, or cannot run here".
731
+
732
+ ## Cloudflare Clef (a second provider)
733
+
734
+ Cloudflare Workers AI hosts Clef, a decision model that takes Jev's request shape
735
+ (`{state, questions}`) and answers with a probability per option. Forge can send chosen
736
+ **sites** (the `site=` name of each call: `jev-drift`, `jev-memory-slice`, ...) to it.
737
+ Without a Cloudflare provider configured, every site asks TypeSafe exactly as before.
738
+
739
+ ```sh
740
+ ./forge jev setup --provider cloudflare --account-id <id> [--model clef|clef-flash] [--sites a,b]
741
+ ./forge jev sites # which provider answers each site, and whether it is ready
742
+ ./forge jev sites jev-drift=typesafe # move a site back
743
+ ./forge jev doctor --live # probes every provider a site uses
744
+ ```
745
+
746
+ - **Token:** a Cloudflare API token with **Account > Workers AI > Read**, in
747
+ `cloudflare.env` beside `jev.json` (`CLOUDFLARE_API_TOKEN=...`, mode `0600`) or in
748
+ `CLOUDFLARE_API_TOKEN`; the account id in config or `CLOUDFLARE_ACCOUNT_ID`. Never argv,
749
+ never a run dir, a prompt or a log: reasons are redacted before they are cut.
750
+ - **Privacy:** a mapped site sends Cloudflare what it would have sent TypeSafe: prompts,
751
+ diff hunks, file excerpts, memory entries. Cloudflare states it does not store or train on
752
+ these requests.
753
+ - **Off switches:** `FORGE_CLEF=off` sends every site back to TypeSafe and stops the
754
+ predictions below; `FORGE_JEV_PROVIDER=typesafe|cloudflare` forces one provider for every
755
+ site; `FORGE_JEV_OUTCOMES=off` stops only the predictions; `FORGE_JEV=off` still stops all.
756
+ - **Limits the client handles:** at most 64 questions per request (drift asks up to 120, so
757
+ it goes as two requests, concurrently, merged); a context of 64K tokens (`clef`) or 24K
758
+ (`clef-flash`) for state and questions together, beyond which Cloudflare cuts the state
759
+ silently, so the client refuses (`too-large`) rather than send it; `4006`/`3036` (allocation
760
+ used up) and `5035` (plan) are final, `3040`/`971` and 429 are retried within the deadline,
761
+ honoring `Retry-After`. Every requested answer must be present and in range, or the call
762
+ is `malformed`. A failure is "no judgment", as with TypeSafe.
763
+ - **Thresholds are per model.** A cutoff measured on Jev's probabilities is not one on
764
+ Clef's: `model_thresholds.<model>.<name>` overrides a threshold for that model's answers.
765
+ Drift now reads `drift_warn`, which falls back to `gate_warn` until a model has its own.
766
+ - **Cost and latency:** `clef` is $0.24 and `clef-flash` $0.038 per million input tokens,
767
+ output free. A drift request (two chunks) takes about 2.4 s at the median and 3.0 s at
768
+ p90, against Jev's 0.65 s and 10.8 s on the same live requests.
769
+
770
+ ### Which sites move: a paired replay, not an assumption
771
+
772
+ `jev-drift` is the only site `setup` maps by default. Measured 2026-10-16 by replaying the
773
+ stored live requests (the run dataset keeps each one whole) through Clef, labeled by the
774
+ task's own outcome, older half of runs to calibrate, newer half to judge; a site moves only
775
+ if Clef is non-inferior (paired run-bootstrap lower bound above -0.02) on at least 30
776
+ labeled decisions from at least 8 held-out runs:
777
+
778
+ | site | labeled | held-out | Clef AUC | Jev AUC | paired diff [95%] | moves |
779
+ |---|---|---|---|---|---|---|
780
+ | `jev-drift` | 4560 decisions, 16 positive, 17 runs | 9 runs | 0.985 | 0.972 | +0.013 [0.000, +0.028] | yes |
781
+ | `jev-memory-slice` | 855 decisions, 144 positive, 10 runs | 5 runs | 0.951 | 0.953 | -0.003 [-0.064, +0.014] | no: too few runs |
782
+
783
+ `jev-review-triage` was not replayed: its `correctness` half has no label, and the export's
784
+ citation label (a review against its own diff or a substituted one) is not a live label.
785
+ Clef keeps drift's shared 0.60 cutoff: on the calibration half neither model warned
786
+ precisely (Jev flagged nothing at 0.60), so no Clef-specific value could be fitted there; at
787
+ 0.60 on the held-out half Clef flagged 1 file (right) and Jev 2 (1 right). The other sites
788
+ stay on TypeSafe until a replay of their own justifies a move.
789
+
790
+ ### Silent outcome predictions
791
+
792
+ With a Cloudflare provider configured, forge also asks Clef two questions forge has never
793
+ asked, and records the answers without acting on them:
794
+
795
+ - `clef-first-pass`, once per plan after the table rewrite that assigns every row its dwarf
796
+ and reviewer: will this task's first attempt pass review? Written to
797
+ `tasks/<id>/clef.first_pass` with the assignment it was about.
798
+ - `clef-retry-pass`, when a dwarf attempt above 1 begins (not on a QA-only resume): will this
799
+ retry pass, given the findings it was handed? Written to `tasks/<id>/clef.retry_pass.<n>`.
800
+
801
+ On forge's exported history Clef ranks these at AUC 0.73 (first pass, 253 cases) and 0.75
802
+ (retry, 300), against hosted Jev's composite at 0.69 and cheap features at 0.67 and 0.61, but
803
+ its raw probabilities are poorly calibrated (retry ECE 0.28) and the newest, largest tasks
804
+ were too few to test. So nothing is printed and nothing acts: no prompt, verdict, tasks table,
805
+ retry spec or findings ever sees a prediction. The export attaches them to the routing and
806
+ retry cases (`first_pass_clef`, with whether attempt 1 ran the predicted dwarf, and
807
+ `retry_pass_clef`), so their skill on new runs can be measured before anything relies on
808
+ them. One bounded command per plan or retry: one deadline, at most 4 requests in flight,
809
+ model resolution cached, an in-process watchdog; about $0.0025 a task with `clef`.
810
+
811
+ ## Status
812
+
813
+ Every phase of the plan is implemented. What changed the picture since is that it was
814
+ finally **run**, rather than tested: the first real `forge-parallel.sh` invocation with
815
+ Jev enabled, on an 8-task plan, found twelve defects that 402 passing tests could not
816
+ see. Most shared one shape — the code was correct and nothing reached it.
817
+
818
+ - Every `--jev*` flag was unreachable: the option parser was sourced from inside
819
+ functions that had already walked their arguments, so `forge_jev_flag`,
820
+ `forge_jev_export`, `forge_jev_record` and `forge_jev_restore` had no callers at all.
821
+ - `prompt.md` never reached routing or the gates, which judged tasks from a one-line
822
+ title. Both text gates had therefore been calibrated on data production never produced.
823
+ - Both memory readers in `verify.py` parsed the *ledger* row format against the rendered
824
+ `memory.md`, so they were empty for every repo, always — which made the `known_flake`
825
+ re-run unreachable, since it requires a corroborating trap.
826
+ - Wave coupling borrowed `gate_warn` and had never fired on any plan.
827
+ - Every memory call was unlogged, and the review-triage flag printed where the default
828
+ output mode never shows it.
829
+
830
+ **What acts today:** the drift, prompt-adequacy and verifiability gates, verification,
831
+ early test feedback, memory curation, wave coupling (same-wave and cross-wave) and
832
+ review-triage annotation. All are advisory —
833
+ none blocks a dispatch, and nothing overturns a `FORGE_VERDICT`.
834
+
835
+ **What was removed (2026-10-17):** the tier rewrite behind `--jev-act`, `calibrate`,
836
+ effort and QA-effort sizing, `parse-spec` and Fractal re-rating. None had ever acted, and
837
+ the run dataset showed decision models cannot choose models or efforts; statistics over
838
+ randomized routing do that now. **Frozen:** wave coupling and forward-dependency scoring
839
+ keep running as they are, and the decision-model fine-tuning pipelines get no further
840
+ work. **Kept and maintained:** capture and replay, the provider client, drift and memory
841
+ curation, where decision models measurably read well.
842
+
843
+ **Known unsound, recorded rather than patched:**
844
+
845
+ - `independently_verifiable` still cannot separate a task that documents later work
846
+ (`readme`, 0.60–0.65) from a real one (`cli`, 0.65–0.75). Putting the plan in state
847
+ fixed the rubric's worst end — it now scores the clearest fragment at 0.26–0.34, where
848
+ it used to score 0.63–0.65 — but the right-hand side remains a precision gate whose
849
+ silence is not evidence.
850
+ - `tests_act` = 0.70 is stale. The semantics are fixed — the gate reads 1 − P(none)
851
+ rather than Choice confidence, so a tie between two correct commands no longer reads as
852
+ doubt — but the threshold behind it was calibrated before candidate evidence reached
853
+ the model, and the one confirmed wrong pick (`spankai` → `cargo test`) has risen from
854
+ 0.55 to 0.77–0.81 and now clears the gate either way.
855
+ - Wave coupling's same-wave half rests on one plan with roughly 0.02 of margin. Its
856
+ cross-wave half (`forward_dependency`, gap 0.73) is far better separated but rests on a
857
+ single positive example.
858
+
859
+ The standing lesson, now learned seven separate times: a threshold named for one rubric
860
+ and reused for another is wrong, and a number measured against state the model is not
861
+ actually sent describes nothing. The sixth was `independently_verifiable`, which asked
862
+ whether a task needed "work declared in a different task" while no different task had
863
+ ever been in state — and the fix was to send the plan, not to reword the question. `jevscope` exists to make both visible — it reads the
864
+ run logs back and reports each rubric's distribution and the spread across byte-identical
865
+ repeated requests, which is the floor any threshold has to clear.