Prevent delayed loop-start message counts from overwriting active Ralph Loop state after the loop has already advanced, so ULW completion can still enter Oracle verification instead of iterating forever.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Cubic AI reviewer flagged the use of the untrusted Host header as the
URL base in startCallbackServer. The server only binds to 127.0.0.1,
so hardcoding "http://127.0.0.1" as the URL base is robust against
malformed or manipulated Host values and matches upstream behavior
prior to the node:http refactor.
Existing test only checked `globalThis.Bun` top-level destructures
and `__require` calls. Add two new test cases:
1. Raw Bun runtime API scanner: scans dist/index.js for any
`Bun.<anyMethod>(` or `Bun.<anyMethod>.` call outside shim-safe
patterns (runtime.Bun, globalThis.Bun, typeof Bun, and the
"Bun is not defined" error-message string). Uses a negative
lookbehind so shim indirection (`runtime.Bun.foo`) passes.
2. Node smoke test: imports dist/index.js under
`node --input-type=module` and asserts stderr contains no
`ReferenceError` and no `Bun is not defined`. The existing
case 3 only checked exit code, which masked lazy-evaluation
crashes that fire after import. Reading exports forces lazy
module-level evaluation paths to run.
bun-install.ts streamToText() was reachable from the plugin bundle via
the cli/config-manager barrel re-export. Replace with the WHATWG
standard `new Response(stream).text()` pattern which works
identically in Bun and Node and avoids the last raw Bun.* runtime
call in dist/index.js.
The OAuth callback server was using Bun.serve, which would crash if
mcp-oauth code paths ever entered the plugin bundle. Switch to
node:http.createServer with the same WHATWG behavior:
- Binds to 127.0.0.1, preserves 200/400/404 status codes
- Translates fetch(Request)→Response to (req, res) callback style
- Clears OAuth timeout on success/error/missing-param paths
- Replaces server.stop(true) with server.close() for shutdown
Functional behavior and response bodies unchanged.
isPortAvailable() previously bound a one-shot Bun.serve and stopped it.
That call was reachable from the plugin bundle through
src/shared/index.ts barrel re-export and crashed on Electron.
Switch to node:net.createServer().listen(port, host), which Bun fully
implements as well. Adds a 2s safety timeout and removes both
"error" and "listening" handlers on resolution to prevent listener
leaks. Behavior is bit-equivalent: returns true iff a server can bind
to (host, port) right now.
Test file is fully rewritten away from stale Bun.serve mocking. New
tests exercise: free-port detection via port 0, EADDRINUSE handling
via a real net.createServer blocker, findAvailablePort range
exhaustion, getAvailableServerPort auto-selection, 127.0.0.1 default
hostname binding, and probe-server resource cleanup.
Eliminates 19 unguarded `Bun.*` runtime call sites in the plugin bundle
that crashed with `ReferenceError: Bun is not defined` under Electron.
Per-tool-call hot paths (executed on every Read/Edit):
- src/tools/hashline-edit/hash-computation.ts: Bun.hash.xxHash32 → bunHashXxh32
- src/tools/hashline-edit/hashline-edit-executor.ts: 8 sites via bunFile/bunWrite
- src/hooks/hashline-read-enhancer/hook.ts: Bun.file → bunFile
- src/hooks/hashline-edit-diff-enhancer/hook.ts: 2 sites via bunFile
Plugin-load paths:
- src/hooks/claude-code-hooks/config.ts and config-loader.ts: Bun.file → bunFile
- src/features/claude-code-mcp-loader/loader.ts: Bun.file → bunFile
- src/features/claude-code-plugin-loader/mcp-server-loader.ts: Bun.file → bunFile
- src/features/team-mode/deps.ts: Bun.spawn → spawn shim
- src/hooks/session-notification-utils.ts: Bun.which → bunWhich, also drops
the bare `declare const Bun` ambient declaration
- src/shared/binary-downloader.ts: Bun.write → bunWrite
Pure mechanical API swaps. No control-flow or signature changes.
The plugin builds with `bun build --target bun` and runs under OpenCode
CLI (Bun SEA) but also under OpenCode Desktop (Electron / Node V8) where
`globalThis.Bun` does not exist. Mirror the existing `bun-spawn-shim.ts`
pattern for three more Bun runtime APIs:
- bun-file-shim: bunFile()/bunWrite() backed by node:fs/promises with
ArrayBuffer slicing to avoid Node Buffer pool exposure
- bun-hash-shim: pure-JS XXH32, bit-exact with Bun.hash.xxHash32 verified
by 1200-pair fuzz comparison so existing hashline LINE#ID tags remain
stable across runtimes
- bun-which-shim: synchronous PATH walker with Windows .exe/.cmd/.bat/.com
extensions plus isUnsafeCommandName guard that rejects path separators,
parent traversal, drive letters and null bytes before any probe
Each shim uses the canonical `runtime.Bun !== undefined` detection and
delegates to native Bun under IS_BUN, otherwise uses Node primitives.
Each ships with a co-located test that exercises both branches via the
`node:vm.runInNewContext` pattern from bun-hash-shim.test.ts.
Handle OpenCode session events that carry the session ID under properties.info.id or properties.info.sessionID so background tasks and continuation hooks do not miss idle/error/delete events.
Add regression coverage for nested session.idle events completing background tasks and waking continuation hooks.
Oracle review of PR #3943 surfaced that endTaskTimer never fires for real Prometheus plans because their canonical path is .sisyphus/plans/ and the snapshot capture was nested inside the !isSisyphusPath branch intended for direct-work warning suppression. Move the snapshot/path tracking out of the warning gate so all plan-file edits are snapshotted regardless of .sisyphus prefix. Keep the warning branch isSisyphus-gated so Atlas does not yell at legitimate plan edits.
Regression test now uses a real .sisyphus/plans/ path and fails against HEAD before the fix.