@autor3search/javascript 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +500 -0
  3. package/bin/autor3search-javascript.js +4 -0
  4. package/package.json +50 -0
  5. package/src/adapters/bench/index.js +31 -0
  6. package/src/adapters/bench/vitest.js +128 -0
  7. package/src/adapters/driver-child.js +72 -0
  8. package/src/adapters/driver-hooks.js +18 -0
  9. package/src/adapters/driver.js +192 -0
  10. package/src/adapters/gates/index.js +64 -0
  11. package/src/adapters/gates/lint.js +46 -0
  12. package/src/adapters/gates/test.js +27 -0
  13. package/src/adapters/gates/typecheck.js +98 -0
  14. package/src/adapters/gates/util.js +45 -0
  15. package/src/adapters/vitest-shim.js +51 -0
  16. package/src/bench/parse.js +120 -0
  17. package/src/bench/set.js +101 -0
  18. package/src/bench/stats.js +443 -0
  19. package/src/cli/cmd-baseline.js +136 -0
  20. package/src/cli/cmd-doctor.js +38 -0
  21. package/src/cli/cmd-eval.js +231 -0
  22. package/src/cli/cmd-init.js +136 -0
  23. package/src/cli/cmd-profile.js +38 -0
  24. package/src/cli/cmd-report.js +96 -0
  25. package/src/cli/cmd-status.js +68 -0
  26. package/src/cli/cmd-stop.js +98 -0
  27. package/src/cli/cmd-version.js +31 -0
  28. package/src/cli/context.js +98 -0
  29. package/src/cli/main.js +62 -0
  30. package/src/config.js +209 -0
  31. package/src/discover.js +239 -0
  32. package/src/doctor.js +282 -0
  33. package/src/duration.js +75 -0
  34. package/src/freeze.js +234 -0
  35. package/src/gitx.js +115 -0
  36. package/src/measure.js +127 -0
  37. package/src/pipeline.js +391 -0
  38. package/src/profile.js +100 -0
  39. package/src/results.js +124 -0
  40. package/src/runner.js +198 -0
  41. package/src/scope.js +92 -0
  42. package/src/state/index.js +312 -0
  43. package/src/state/lock.js +189 -0
  44. package/src/state/stop.js +56 -0
  45. package/src/verdict.js +214 -0
  46. package/templates/program.md +264 -0
package/src/verdict.js ADDED
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Turns gate outcomes and measurements into a single decision.
3
+ *
4
+ * This module is the contract with the agent: program.md documents these
5
+ * status strings, these reason codes and these exit codes, and the agent's
6
+ * loop branches on them. They are frozen — renaming one is a breaking change
7
+ * to templates/program.md and must happen in the same commit.
8
+ */
9
+ import { countForAlpha, minAchievableP } from './bench/stats.js'
10
+
11
+ /** The terminal outcomes of one experiment. */
12
+ export const STATUS = {
13
+ KEEP: 'KEEP',
14
+ DISCARD: 'DISCARD',
15
+ FAIL: 'FAIL',
16
+ CRASH: 'CRASH',
17
+ /** Not a verdict: an experiment interrupted before it was measured. */
18
+ ABORTED: 'ABORTED',
19
+ }
20
+
21
+ /** Machine-readable explanations, grouped by the stage that produces them. */
22
+ export const REASON = {
23
+ IMPROVED: 'improved',
24
+ NO_IMPROVEMENT: 'no_significant_improvement',
25
+ BELOW_MIN_EFFECT: 'improvement_below_min_effect',
26
+ GUARD_REGRESSION: 'guard_regression',
27
+ SCOPE: 'scope_violation',
28
+ CONFIG_CHANGED: 'config_changed',
29
+ NEW_TEST_FILE: 'new_test_file',
30
+ MISSING_TEST_FILE: 'missing_test_file',
31
+ SYMLINK_SWAP: 'symlink_swap',
32
+ HARDLINK_SWAP: 'hardlink_swap',
33
+ FROZEN_TAMPERED: 'frozen_store_tampered',
34
+ BASELINE_TAMPERED: 'baseline_tampered',
35
+ TYPECHECK: 'typecheck_failed',
36
+ LINT: 'lint_failed',
37
+ /** The benchmark run itself failed, so there is nothing to score. */
38
+ MEASUREMENT: 'measurement_failed',
39
+ TESTS: 'tests_failed',
40
+ TIMEOUT: 'timeout',
41
+ STOP_FORCED: 'stop_forced',
42
+ }
43
+
44
+ /**
45
+ * Builds a Result for a failed correctness stage, before measurement.
46
+ *
47
+ * @param {string} status
48
+ * @param {string} reason
49
+ * @param {string} message
50
+ */
51
+ export function gate(status, reason, message) {
52
+ return { status, reason, score: 0, message, regressions: [], warnings: [] }
53
+ }
54
+
55
+ /**
56
+ * Applies the scoring rules.
57
+ *
58
+ * 1. Any regression significant at the RAW, uncorrected alpha and larger than
59
+ * maxRegressPct rejects the change, however good the overall score. This
60
+ * check deliberately does not apply the Bonferroni correction from rule 2:
61
+ * Bonferroni only ever makes it harder to call a result significant, and
62
+ * applying it here would make the guard LESS sensitive to harm — backwards
63
+ * from what a guard is for. The asymmetry is intentional: be conservative
64
+ * about accepting a win, be liberal about catching a regression.
65
+ *
66
+ * 2. Otherwise keep only when BOTH:
67
+ * a. the score is a real speedup by at least minEffectPct — below
68
+ * 1 - minEffectPct/100, not merely below 1. A result that is
69
+ * technically significant but trivially small is not worth a commit in
70
+ * an unattended loop.
71
+ * b. at least one benchmark improved at the Bonferroni-corrected
72
+ * threshold alpha/k, where k is the number of benchmarks compared.
73
+ * Comparing k benchmarks against the same uncorrected alpha inflates
74
+ * the family-wise false-positive rate — with k = 4, roughly an 18%
75
+ * chance at least one shows a spurious "significant" improvement even
76
+ * when nothing changed.
77
+ *
78
+ * A change clearing 2b but missing 2a discards with BELOW_MIN_EFFECT rather
79
+ * than NO_IMPROVEMENT: it measurably worked, it was just too small to bank,
80
+ * and those call for different next moves.
81
+ *
82
+ * @param {{deltas: object[], score: number, maxRegressPct: number, minEffectPct: number}} input
83
+ */
84
+ export function decide(input) {
85
+ const deltas = input.deltas ?? []
86
+ // k is the family size for the Bonferroni correction. compareAll never
87
+ // returns an empty list, but guard against division by zero anyway.
88
+ const k = Math.max(1, deltas.length)
89
+ const warnings = measurementWarnings(deltas, k)
90
+ const score = input.score
91
+
92
+ const regressions = deltas.filter((d) => d.significant && d.pctChange > input.maxRegressPct)
93
+ if (regressions.length > 0) {
94
+ const detail = regressions.map((d) => `${d.name} ${signed(d.pctChange, 1)}%`).join(', ')
95
+ return {
96
+ status: STATUS.DISCARD,
97
+ reason: REASON.GUARD_REGRESSION,
98
+ score,
99
+ message: `regression guard tripped (limit ${signed(input.maxRegressPct, 1)}%): ${detail}`,
100
+ regressions,
101
+ warnings,
102
+ }
103
+ }
104
+
105
+ const improved = deltas.some((d) => d.pctChange < 0 && d.p < d.alpha / k)
106
+ const minEffectThreshold = 1 - input.minEffectPct / 100
107
+
108
+ if (score < minEffectThreshold && improved) {
109
+ return {
110
+ status: STATUS.KEEP,
111
+ reason: REASON.IMPROVED,
112
+ score,
113
+ message: `score ${score.toFixed(4)} (${signed((score - 1) * 100, 2)}%)`,
114
+ regressions: [],
115
+ warnings,
116
+ }
117
+ }
118
+
119
+ if (improved && score < 1) {
120
+ return {
121
+ status: STATUS.DISCARD,
122
+ reason: REASON.BELOW_MIN_EFFECT,
123
+ score,
124
+ message:
125
+ `score ${score.toFixed(4)} (${signed((score - 1) * 100, 2)}%), a real improvement but below ` +
126
+ `the ${input.minEffectPct.toFixed(1)}% minimum effect size`,
127
+ regressions: [],
128
+ warnings,
129
+ }
130
+ }
131
+
132
+ return {
133
+ status: STATUS.DISCARD,
134
+ reason: REASON.NO_IMPROVEMENT,
135
+ score,
136
+ message: `score ${score.toFixed(4)} (${signed((score - 1) * 100, 2)}%), no significant improvement`,
137
+ regressions: [],
138
+ warnings,
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Gathers everything that qualifies how far the numbers in a Result can be
144
+ * trusted: each comparison's own warnings, then the check that a KEEP was
145
+ * statistically reachable at all. These never change the decision — they say
146
+ * what it can and cannot mean.
147
+ */
148
+ function measurementWarnings(deltas, k) {
149
+ const out = [...new Set(deltas.flatMap((d) => d.warnings ?? []))]
150
+ const unreachable = unreachableAlphaWarning(deltas, k)
151
+ if (unreachable) out.push(unreachable)
152
+ return out
153
+ }
154
+
155
+ /**
156
+ * Reports when rule 2b cannot be satisfied by any result whatsoever, so the
157
+ * run is incapable of a KEEP before it starts.
158
+ *
159
+ * config.validate enforces a count floor for a single benchmark; this is the
160
+ * same footgun at k benchmarks, which the validator cannot see because it does
161
+ * not know how many benchmarks a run will compare. A KEEP needs only ONE
162
+ * benchmark to clear the threshold, so this warns only when none of them can.
163
+ */
164
+ function unreachableAlphaWarning(deltas, k) {
165
+ if (deltas.length === 0) return null
166
+ let worstN = 0
167
+ let alpha = 0
168
+ for (const d of deltas) {
169
+ const corrected = d.alpha / k
170
+ // This one can clear it; that is enough for a KEEP to be possible.
171
+ if (minAchievableP(d.nBase, d.nCand) < corrected) return null
172
+ const n = Math.min(d.nBase, d.nCand)
173
+ if (n > worstN) {
174
+ worstN = n
175
+ alpha = d.alpha
176
+ }
177
+ }
178
+ const corrected = alpha / k
179
+ const need = countForAlpha(corrected)
180
+ const tail = need > 0 ? ` — raise count to at least ${need}` : ' — raise count, or measure fewer benchmarks'
181
+ return (
182
+ `no KEEP was reachable: comparing ${k} benchmark(s) corrects the significance threshold to ` +
183
+ `${corrected.toFixed(5)}, but with ${worstN} rounds per side the test cannot produce a p-value ` +
184
+ `below ${minAchievableP(worstN, worstN).toFixed(5)} however large the improvement is${tail}`
185
+ )
186
+ }
187
+
188
+ /**
189
+ * Maps a Result to the process exit code program.md documents.
190
+ *
191
+ * Anything unrecognised — including ABORTED, which is an interrupted
192
+ * experiment rather than a verdict — maps to FAIL. Never to 0: a status the
193
+ * harness cannot classify must not read to the agent as a successful KEEP.
194
+ *
195
+ * @param {{status: string}} result
196
+ * @returns {number}
197
+ */
198
+ export function exitCode(result) {
199
+ switch (result.status) {
200
+ case STATUS.KEEP:
201
+ return 0
202
+ case STATUS.DISCARD:
203
+ return 1
204
+ case STATUS.CRASH:
205
+ return 3
206
+ default:
207
+ return 2
208
+ }
209
+ }
210
+
211
+ /** Formats a number with an explicit sign, e.g. "+12.0", "-8.77". */
212
+ function signed(n, digits) {
213
+ return `${n >= 0 ? '+' : ''}${n.toFixed(digits)}`
214
+ }
@@ -0,0 +1,264 @@
1
+ # program.md
2
+
3
+ This file is your instructions, coding agent. Read it fully before doing
4
+ anything. It is the only thing a human edits to steer this run — everything
5
+ else (the metric, the gates, the verdict) belongs to the harness.
6
+
7
+ ## Setup
8
+
9
+ Every command below is written as `autor3search-javascript <cmd>`. If it is
10
+ not on your PATH, prefix each one with `npx` — `npx autor3search-javascript
11
+ status` — and keep using the same form for the whole run, so the version that
12
+ recorded the baseline is the version that scores against it.
13
+
14
+ Before starting the loop, do this once:
15
+
16
+ 1. Agree a run tag with the human if one was not already given (a short slug
17
+ like `sep7` — today's date or similar is fine).
18
+ 2. Confirm `autor3search-javascript baseline -tag <tag>` has already been run
19
+ for this tag. If it has not, stop and ask the human to run it, or run it
20
+ yourself if you have been told you may. `baseline` creates the run branch,
21
+ freezes the current test AND benchmark files as golden copies, and records
22
+ the commit this run measures against. Everything downstream depends on this
23
+ having happened exactly once.
24
+ 3. Read the repository. Skim the files matched by `scope` in
25
+ `.autor3search/config.yaml`. Run `autor3search-javascript profile` once so
26
+ you know what you are starting from before changing anything.
27
+
28
+ ## Experimentation
29
+
30
+ What you MAY do:
31
+
32
+ - Edit any source file matched by a `scope` pattern in
33
+ `.autor3search/config.yaml`.
34
+ - Add new files inside `scope`, as long as they are not test or benchmark
35
+ files.
36
+ - Run any read-only diagnostic (`autor3search-javascript profile`,
37
+ `node --cpu-prof`, reading `run.log`) as often as you like between
38
+ experiments.
39
+
40
+ What you MUST NOT do:
41
+
42
+ - **Edit any `*.test.*`, `*.spec.*` or `*.bench.*` file.** All of them are
43
+ restored from the frozen baseline copies before every `eval`, so an edit
44
+ there is silently discarded and wastes an experiment slot. The benchmark
45
+ files matter as much as the tests: they define the metric. If a test looks
46
+ wrong, say so in your `-desc` and move on; do not try to route around it.
47
+ - **Edit `package.json` or any lockfile, ever.** These are rejected outright
48
+ regardless of `scope`. Changing a dependency is a supply-chain decision a
49
+ human makes, and a swapped dependency changes *what* is measured, not just
50
+ how fast it runs.
51
+ - **Edit `.autor3search/config.yaml`.** Its hash is recorded at `baseline`
52
+ time; if it has changed by `eval` time the run fails with reason
53
+ `config_changed`.
54
+ - Edit an ordinary source file outside `scope`. That is what the scope gate
55
+ rejects, failing the experiment before it is even measured.
56
+ - Try to weaken, disable or reinterpret the verdict. `eval`'s exit code and
57
+ `--json` output are the only truth.
58
+ - Batch multiple unrelated changes into one experiment. One idea per
59
+ experiment keeps every result attributable and every discard cheap.
60
+
61
+ ## Output format
62
+
63
+ `autor3search-javascript eval` is the only command whose result decides
64
+ anything. It exits with one of four codes:
65
+
66
+ | Exit code | Meaning | Verdict status |
67
+ |---|---|---|
68
+ | `0` | KEEP — a real, safe improvement | `KEEP` |
69
+ | `1` | DISCARD — no significant improvement, too small to bank, or a regression | `DISCARD` |
70
+ | `2` | FAIL — a gate rejected the change: scope, config, tampering with a frozen file, a new or missing test/bench file, lint, or a failing test | `FAIL` |
71
+ | `3` | CRASH — the typecheck failed outright, a phase timed out, or the benchmark measurement itself failed | `CRASH` |
72
+
73
+ Any status the harness cannot classify arrives as exit code `2`. `ABORTED`
74
+ (see "Stopping") does too, for exactly that reason: it is not a verdict, and
75
+ treating it like FAIL is the right thing to do with it.
76
+
77
+ **A `KEEP` requires a real, not just a technically significant, improvement.**
78
+ `eval` returns `KEEP` only when `score` clears `1 - min_effect_pct/100`
79
+ (default 1%, i.e. score < 0.99) AND at least one benchmark improved past a
80
+ Bonferroni-corrected significance bar. A change that shaves off a fraction of
81
+ a percent is `DISCARD`ed by design — do not spend a night chasing sub-1% wins.
82
+ And even a `KEEP` is evidence, not proof: any significance threshold admits
83
+ some false positives.
84
+
85
+ With `--json`, `eval` prints one JSON object to stdout and nothing else:
86
+
87
+ ```json
88
+ {
89
+ "status": "KEEP",
90
+ "reason": "improved",
91
+ "score": 0.9123,
92
+ "message": "score 0.9123 (-8.77%)",
93
+ "regressions": [],
94
+ "warnings": [],
95
+ "stop_requested": false,
96
+ "run": {
97
+ "tag": "sep7",
98
+ "branch": "autor3search-javascript/sep7",
99
+ "baseline_commit": "a3f1c2d",
100
+ "measure_commit": "9b7e410",
101
+ "worktree": "/Users/you/Library/Caches/autor3search-javascript/1a2b3c4d/sep7/baseline-worktree",
102
+ "experiment": 5
103
+ }
104
+ }
105
+ ```
106
+
107
+ `reason` is a stable machine-readable code: `improved`,
108
+ `no_significant_improvement`, `improvement_below_min_effect`,
109
+ `guard_regression`, `scope_violation`, `config_changed`, `new_test_file`,
110
+ `missing_test_file`, `symlink_swap`, `hardlink_swap`, `frozen_store_tampered`,
111
+ `baseline_tampered`, `typecheck_failed`, `lint_failed`, `tests_failed`,
112
+ `measurement_failed`, `timeout`, `stop_forced`.
113
+
114
+ Two discard reasons mean genuinely different things.
115
+ `no_significant_improvement` means nothing measurably moved — the idea did not
116
+ work, drop it. `improvement_below_min_effect` means it DID work and the
117
+ harness measured a real speedup, just smaller than `min_effect_pct` will bank.
118
+ That says the direction is right: a variation with a larger effect, or the
119
+ same idea applied to a hotter path, may well clear the bar. Do not read it as
120
+ failure.
121
+
122
+ `score` is the geometric mean of `candidate_time / baseline_time` across the
123
+ declared benchmarks — below 1 is faster.
124
+
125
+ `warnings`, when present, says the measurement is too weak to carry the
126
+ verdict printed beside it. They never change the decision; they tell you not
127
+ to over-read it. Two you may see:
128
+
129
+ - **too few observations for a confidence interval** — the medians are real
130
+ but the interval around them is unbounded. Raise `count`.
131
+ - **no KEEP was reachable** — the significance threshold is corrected for the
132
+ number of benchmarks compared (`alpha/k`), and with the configured `count`
133
+ the test cannot produce a p-value that small however large the improvement
134
+ is. Every experiment will `DISCARD` until `count` is raised. Treat this as a
135
+ broken configuration and stop rather than burning the night on experiments
136
+ that cannot be banked.
137
+
138
+ **What the baseline means changes as the run progresses.** It is NOT always
139
+ the commit `baseline` recorded — it is whatever the measurement baseline
140
+ currently points to, and a KEEP moves that pointer to the commit you just
141
+ kept. So `score` always answers "did THIS experiment help, compared to the
142
+ last thing that was kept" — never "is the tree better than when the run
143
+ started." Two consequences: after a KEEP, running `eval` again with nothing
144
+ new committed measures your last commit against itself and correctly
145
+ `DISCARD`s — that is not a bug; and a long run's total progress is the
146
+ *product* of every kept `score`, which is what `report` computes.
147
+
148
+ `stop_requested` is the one field that changes what you do. See below.
149
+
150
+ ## Stopping
151
+
152
+ The loop does not end on its own. It ends in one of two ways.
153
+
154
+ **A graceful stop.** The human runs `autor3search-javascript stop`. That
155
+ writes a request `eval` reports back as `"stop_requested": true`, alongside a
156
+ verdict that is still fully valid. When you see it:
157
+
158
+ 1. Apply the verdict exactly as you would have anyway — KEEP leaves the
159
+ commit, anything else is `git reset --hard HEAD~1`. A stop must never leave
160
+ a commit on the branch that nothing decided on.
161
+ 2. Do NOT start another experiment.
162
+ 3. Run `autor3search-javascript report` and summarize in a few lines: what you
163
+ tried, what was kept, what you would try next.
164
+ 4. Exit the loop and say you stopped because the human asked.
165
+
166
+ **An interrupt.** Ctrl+C, or `autor3search-javascript stop --force`. Either
167
+ cancels `eval` mid-experiment. You will see `"status": "ABORTED"` with
168
+ `"reason": "stop_forced"`, exit code `2`, and no `results.tsv` row — nothing
169
+ was measured, so nothing was recorded. Treat the commit as you would any FAIL
170
+ (`git reset --hard HEAD~1`), then stop as above.
171
+
172
+ If the human wants the run to continue after all, they clear the request with
173
+ `autor3search-javascript stop --clear`. That is their decision, not something
174
+ to wait for or ask about.
175
+
176
+ ## Logging
177
+
178
+ `results.tsv` is HARNESS-OWNED. `eval` appends exactly one row on every
179
+ invocation, KEEP or not. Never create, append to, or edit it yourself — a
180
+ manual write is either redundant or corrupts a file the harness parses
181
+ strictly, breaking the human's morning `report` on the whole file.
182
+
183
+ The one column that is yours is `description`, set with `-desc`:
184
+
185
+ ```
186
+ autor3search-javascript eval --json -desc "preallocate the map"
187
+ ```
188
+
189
+ Always pass `-desc`, on every invocation, KEEP or not — it is the only record
190
+ of what you were trying. Keep it terse and never dishonest: describe what you
191
+ actually tried, including for a DISCARD, FAIL or CRASH. A long trail of honest
192
+ discards is more useful to the human than a short trail that hides them.
193
+
194
+ ## JavaScript optimization idea bank
195
+
196
+ Measure first, then reach for these:
197
+
198
+ - **Ask whether the work is needed at all.** An algorithmic change, or
199
+ skipping work entirely, usually beats every micro-optimization below.
200
+ - **Allocation in hot loops.** Every intermediate array, object literal and
201
+ closure allocates. `autor3search-javascript profile` reports an approximate
202
+ `bytes/op` alongside the timings.
203
+ - **Avoid intermediate arrays.** `arr.map(f).filter(g).reduce(h)` walks the
204
+ data three times and allocates twice. One loop does not.
205
+ - **Keep object shapes stable.** Assigning properties in a consistent order,
206
+ and never deleting them, keeps V8 on a monomorphic hidden class. A
207
+ polymorphic call site is far slower than a monomorphic one.
208
+ - **Preallocate.** `new Array(n)` when n is known, and typed arrays
209
+ (`Float64Array`, `Uint8Array`) for numeric data — they avoid boxing entirely.
210
+ - **Hoist closures out of loops.** A function expression created inside a loop
211
+ allocates on every iteration.
212
+ - **String building.** Repeated `+=` in a long loop can be quadratic; collect
213
+ into an array and `join('')`, or build with a fixed-size buffer.
214
+ - **`Map` vs plain object vs array.** For small fixed key sets a linear scan
215
+ over an array beats both. `Map` beats an object for frequent insertion and
216
+ deletion of non-identifier keys. Measure at your real size.
217
+ - **Avoid `try`/`catch` and `arguments` in the hottest function** if it stops
218
+ V8 optimizing it — check with `--trace-deopt` before assuming.
219
+ - **Regular expressions.** Compile once outside the loop; a `RegExp` literal
220
+ inside a loop with the `g` flag also carries mutable `lastIndex` state.
221
+ - **Character codes over string methods.** `charCodeAt` and numeric comparison
222
+ avoid the allocation `toLowerCase()` on a single character causes.
223
+
224
+ ## The experiment loop
225
+
226
+ `eval` splits its own output for you: the verdict — one compact JSON object —
227
+ goes to stdout, and the full build/lint/test/benchmark transcript goes to
228
+ `run.log` in the repository root. `run.log` is the harness's own file, opened
229
+ by `eval` before it does anything else. **Never redirect `eval`'s stdout into
230
+ it** (`eval --json > run.log 2>&1` or similar): that opens a second descriptor
231
+ on a path the harness already holds open, and whichever writes second
232
+ overwrites the other from byte 0 — destroying exactly the transcript you need
233
+ when something FAILs or CRASHes. Run `eval --json` bare and read the verdict
234
+ from its stdout; open `run.log` only to read.
235
+
236
+ LOOP FOREVER:
237
+
238
+ 0. Print one context line so the human watching knows where the run is:
239
+ `[exp <n> | <branch> | vs <measure_commit> | stop: autor3search-javascript stop]`
240
+ Take the numbers from the previous experiment's `run` object; on the first
241
+ pass, from `autor3search-javascript status`.
242
+ 1. Confirm you are on the run branch.
243
+ 2. If you have no strong hypothesis, run `autor3search-javascript profile` and
244
+ read the hot spots.
245
+ 3. Change ONE thing in the in-scope source. One idea per experiment.
246
+ 4. `git add -A && git commit -m "<idea>"`
247
+ 5. `autor3search-javascript eval --json -desc "<idea>"` — no redirect.
248
+ 6. Read the verdict from stdout: one compact JSON object.
249
+ 7. KEEP → leave the commit; the branch advances and the measurement baseline
250
+ advances to this commit, so your next experiment is measured against what
251
+ you just kept.
252
+ Anything else → `git reset --hard HEAD~1`
253
+ 8. If the verdict carried `"stop_requested": true`, or its status was
254
+ `ABORTED`, follow "Stopping" and leave the loop.
255
+ 9. Go to 0.
256
+
257
+ **NEVER STOP ON YOUR OWN.** Do not pause to ask whether to continue. The human
258
+ may be asleep. You are autonomous. If you run out of ideas, re-read the
259
+ profile output, re-read the idea bank, combine previous near-misses, or try a
260
+ more radical change.
261
+
262
+ Only the human ends this loop, and only in the two ways "Stopping" describes.
263
+ Running out of ideas is not one of them, and neither is a long string of
264
+ DISCARDs.