diff --git a/toolchain/instagram/oskiewar-clockwork.sh b/toolchain/instagram/oskiewar-clockwork.sh index be324efbbe..086d871b58 100755 --- a/toolchain/instagram/oskiewar-clockwork.sh +++ b/toolchain/instagram/oskiewar-clockwork.sh @@ -3,6 +3,7 @@ # # oskiewar-clockwork publish 0 # bake slot 0 on the oven and post it # oskiewar-clockwork insights # pull Meta's figures onto the ledger +# oskiewar-clockwork tune # price the climb bot if reels failed (proposes only) # # This exists because the crontab used to call `bash -lc '... node ...'` directly # and every part of that was wrong in a way that failed silently: @@ -112,8 +113,25 @@ case "$MODE" in status=${PIPESTATUS[0]} [ "$status" -eq 0 ] || exit "$status" ;; + tune) + # Reads the day's outcomes off the ledger and, only if reels failed, prices + # the climb bot's constants headlessly. It PROPOSES ONLY — it writes + # ~/.local/state/oskiewar-survival-tune.json and changes nothing else — so + # it has no ledger to publish and no figures to post, and stops here. + # + # This needs the whole game shell, not just the marketing tools: the lab + # serves oskiewar.js and the modules `shell.mjs` lists out of + # system/public. Those paths were added to this checkout's sparse cone for + # exactly this; without them the page 404s and never boots. + STAGE="pricing the climb bot against recent outcomes" + "$NODE" xbox/live/marketing/survival-tune.mjs 2>&1 | tee -a "$LOG" + status=${PIPESTATUS[0]} + STAGE="done" + say "✓ tune complete" + exit "$status" + ;; *) - say "unknown mode '$MODE' (want: publish | insights)"; exit 2 ;; + say "unknown mode '$MODE' (want: publish | insights | tune)"; exit 2 ;; esac # --- send the figures where the wall can see them now --- diff --git a/xbox/live/marketing/survival-lab.mjs b/xbox/live/marketing/survival-lab.mjs index 936f15b7be..ae76ec4568 100644 --- a/xbox/live/marketing/survival-lab.mjs +++ b/xbox/live/marketing/survival-lab.mjs @@ -18,7 +18,7 @@ // That table is the whole point: a bot change ships because a number moved, // not because a run looked better. -import { existsSync } from "node:fs"; +import { existsSync, realpathSync } from "node:fs"; import { resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { serveShell, repo, survivalLadder, survivalLevelFor } from "./shell.mjs"; @@ -198,12 +198,24 @@ export async function openLab({ log = () => {} } = {}) { export { levelFor, survivalLevelCount }; // Everything below is the command line. Imported, this module is just the lab. -const invoked = process.argv[1] && - resolve(process.argv[1]) === fileURLToPath(import.meta.url); +// +// `import.meta.url` reports the REAL path while `argv[1]` keeps whatever the +// caller typed, so comparing them raw silently decides "imported" whenever any +// path component is a symlink — /tmp against /private/tmp on a Mac, or a +// worktree reached through a linked directory. The script then exits 0 having +// printed nothing, which reads exactly like a passing run. Ask node when it can +// answer, and compare real paths when it cannot. +const invoked = typeof import.meta.main === "boolean" ? import.meta.main : (() => { + try { + return Boolean(process.argv[1]) && + realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url)); + } catch { return false; } +})(); if (invoked) { const log = console.log; const lab = await openLab({ log }); const climbOne = (options) => lab.climb(options); +let failed = false; try { const runs = Math.max(1, Number(flags.runs || 3)); @@ -244,8 +256,18 @@ try { results.push(await climbOne({ seed, tune, cap })); report(Object.keys(tune).length ? "tuned" : "shipping bot", results); } +} catch (error) { + // `process.exit(0)` in the finally used to run before a rejection could be + // reported, so a lab that never got the page open printed nothing and exited + // 0 — a broken run and a clean run were the same output. Say what went wrong + // and exit non-zero, because a sweep is only worth anything if its silence + // means "no summits", not "no simulation". + failed = true; + console.error(`✗ ${error?.stack || error?.message || error}`); } finally { await lab.close(); - process.exit(0); + // Chrome and the shell can both leave handles behind; exit explicitly rather + // than waiting on the loop to drain. + process.exit(failed ? 1 : 0); } }