Technical Museum
All Hardware Maps ML & AI Music Social Tools Writing Apps & Games
Deciduous
Decision graph CLI for tracing how software decisions evolve
Rust SQLite HTML/JS
Deciduous Archaeology Demo
Demonstrations of decision archaeology on React and stacked git workflows
Rust React Git
Phish Explorer
Jam analytics dashboard for Phish 3.0 era
Elixir Phoenix LiveView D3.js
Local LLM on MacBook
4-bit quantization, safetensors, and Bumblebee + EMLX for Apple Silicon
Elixir Rust Python
A Bot that posts like me
Porting the posting bot to Elixir using local LLM work
Elixir Bumblebee EMLX
Receipt Printer Software Suite
A complete software suite for thermal receipt printers
Elixir Python
Role Call
TV writer overlap explorer
Elixir Phoenix LiveView
Fill Your Sky
Interactive map of 418+ Bluesky communities with 545K+ people
Elixir Phoenix LiveView D3.js
Code Mirror
A live code mirror experiment
Elixir Phoenix LiveView
Pocket Pnin
A local LLM running on my iPhone, coming to the App Store for free
Swift MLX
NYC Census Maps
Interactive census and PLUTO data visualization for New York City
Elixir Phoenix LiveView Leaflet.js
MTA Bus Tracker
Real-time MTA bus and train tracking on an interactive map
Elixir Phoenix LiveView Leaflet.js
Concert GIF Maker
Extract GIFs from concert videos with a retro Mac interface
Elixir Phoenix LiveView FFmpeg
Send a VERY direct message, to my receipt printer
A social project where friends send photos that print on my receipt printer
Elixir Phoenix LiveView
Archive TV
A real over-the-air TV channel from magnetic media archives
Elixir FFmpeg
Losselot
Neural network loss function explorer
Python
Todoinksies
A personal todo app
Elixir Phoenix LiveView
Ormery
An ORM written in Temper
Temper
Collage Maker
Upload photos and arrange them into grid collages
Elixir Phoenix LiveView
GenStage Tutorial 2025
A modern GenStage tutorial for the Elixir ecosystem
Elixir GenStage
Temper Rust Bug
Found a bug in the Temper compiler and built a demo repo
Temper Rust
300+ Years of Tree Law
A blog post that became its own LiveView application
Elixir Phoenix LiveView
HEEx in Other Languages
Experiments porting Phoenix HEEx templates to Rust, Lua, C#, Java, Python, and JS
Temper Rust Lua
Live Draft LSP
Live-stream blog drafts from Zed to Phoenix via a custom LSP
Rust Elixir Zed
Bluesky Hoover Apps
Various apps that vacuum up and process the Bluesky firehose
Elixir Phoenix LiveView
Bobby Posts Bot
A bot that posts like me, in Python
Python
Photo Booth Receipt Printer
A portable photo booth that prints on receipt paper
Python Elixir
Nathan For Us
A Nathan For You social network with video search and GIF creation
Elixir Phoenix LiveView FFmpeg
Browser History Roast MCP
An MCP server that roasts you based on your browser history
Python MCP
GenStage Tutorial (Original)
The original GenStage tutorial
Elixir GenStage
Bluesky Firehose Toys
Real-time firehose visualizations: emoji streams, jetstream comparisons, and more
Elixir Phoenix LiveView WebSocket
31 exhibits Apps & Games
MY FAV
LEICA SHOTS
Work Log — Recent Commits
+6665-2658 temperlang/temper/do-more-crimes-to-play-snake-multiplayer Mar 20 03:57
8ed2fd4 feat: replace tungstenite with hand-rolled WebSocket over raw TCP tungstenite corrupts internal state when read/write are interleaved from different threads, even with Mutex protection. Replace it with a minimal WebSocket implementation (~150 lines) that: - Does HTTP upgrade handshake byte-by-byte (avoids BufReader read-ahead) - Uses TcpStream::try_clone() to split into independent read/write halves - Reader thread owns the read half, writer Mutex holds the write half - No shared internal framing state — reads and writes are independent - SHA-1 for the accept key via sha1_smol (only external dep) Also removes the sleep() debug logging. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
e76ab32 fix: add ws npm dependency to temper-core package.json The ws.js support file dynamically imports the 'ws' package, but it wasn't listed as a dependency. After temper build regenerated the output, npm install wouldn't install ws, causing wsListen/wsConnect to fail silently at runtime. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
9bb8efa chore: remove debug logging from Rust ws support Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
936910d fix: guard wsSend against closed WebSocket connections Check readyState before calling send(), and wrap in try/catch to prevent synchronous throws from crashing the server when a client disconnects mid-broadcast. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
d9f3020 fix: channel-based I/O for Rust WebSocket connections Replace Mutex-shared WebSocket with a dedicated I/O thread per connection that communicates via mpsc channels. The I/O thread owns the WebSocket exclusively and polls for both send and recv, avoiding tungstenite's internal state corruption from concurrent access. Send is now non-blocking (channel push), recv blocks on the channel receiver in a spawned thread. This fixes the ResetWithoutClosingHandshake error that occurred when the server's recv loop and send loop competed for the socket. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
7718c2f fix: spawn OS thread for Rust nextKeypress to avoid blocking async runner The Rust backend uses a SingleThreadAsyncRunner, so the blocking crossterm::event::read() call was starving the game tick coroutine. Spawn a real OS thread for the blocking read and complete the promise from there, allowing other async tasks to proceed concurrently. Signed-off-by: Robert Grayson <bobbbygrayson+github@gmail.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Robert Grayson <bobbbygrayson+github@gmail.com>
03f6b99 feat: add std/ws WebSocket module and std/io terminal size detection Rebased onto do-crimes-to-play-snake: resolved conflicts with std/keyboard, updated Rust terminal size to use crossterm instead of libc, added ws to stdSupportNeeders set.
561c49a feat: add Python backend for std/ws and std/io terminal size Wire WebSocket support (wsListen, wsAccept, wsConnect, wsSend, wsRecv, wsClose) and terminal size detection (terminalColumns, terminalRows) for the Python backend. Uses hand-rolled WebSocket over raw TCP (same approach as Rust) with socket.dup() for read/write split and a dedicated reader thread per connection. No external dependencies — just stdlib socket, hashlib, base64, struct, threading. Terminal size uses shutil.get_terminal_size(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
3a512b1 fix: use dedicated threads for Rust ws operations Temper's Rust runtime uses a single-threaded task runner. Blocking ws operations (accept, send, recv) would block the runner and prevent other async blocks (like readLine) from processing. Spawn dedicated threads for ws_accept, ws_send, and ws_recv instead of going through crate::run_async. This lets the WS operations block independently while other async work continues. Also adds error logging to ws_recv for debugging. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
9a9b350 fix: set read timeout on WS connections for Rust backend Both server-accepted and client connections now have a 50ms read timeout. This prevents the recv loop from holding the socket Mutex indefinitely, allowing send operations to interleave. Without this, the Rust server couldn't send frames because recv blocked the lock. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
7fceb70 test: update snapshots for io.js and keyboard.js in CLI tests JsRunFileLayoutTest: add io.js and keyboard.js to expected file trees in both std/ and temper-core/ directories. ReplTest: allow scheduler call in Lua translation output. Signed-off-by: Robert Grayson <bobbbygrayson+github@gmail.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Robert Grayson <bobbbygrayson+github@gmail.com>
73b848d fix: Rust ws support - rename functions, fix visibility and stream types - Rename support functions to std_ws_* to avoid colliding with Temper-generated panic stubs (same pattern as std_sleep/std_read_line) - Change visibility from pub(crate) to pub for cross-crate access - Accept &dyn WsServerTrait/WsConnectionTrait instead of &WsServer to work with the Rust backend's interface deref codegen - Use WsStream enum to handle both WebSocket<TcpStream> (server-accepted) and WebSocket<MaybeTlsStream<TcpStream>> (client-connected) - Use temper_core::cast() for downcasting instead of manual as_any chain - Add .into() for tungstenite 0.26 Message::Text(Utf8Bytes) change Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
+5731-2657 temperlang/temper/do-crimes-to-play-snake Mar 19 22:22
09dfee2 remove debug cruft Signed-off-by: Mike Samuel <mikesamuel@gmail.com>
53e5d99 fix test goldens Signed-off-by: Mike Samuel <mikesamuel@gmail.com>
eafa620 ran code formatter Signed-off-by: Mike Samuel <mikesamuel@gmail.com>
91cebe8 astbuild: Reworked expression inversion for MQ strings Previously, TokenSourceAdapter assumed brackets, (`{:` ... `:}`) around statement fragments. Now we have margin characters that, in a left to right scan, give enough context. This reworks *TokenSourceAdapter* to cue off the margin characters when inverting statements in complex string expressions, and to differentiate between `~` and `"` margin characters when stripping incidental whitespace. It also updates documentation around string expression syntax, semantics, and whitespace elision. Signed-off-by: Mike Samuel <mikesamuel@gmail.com>
70c75bb test: add functional test for `std/io` `sleep()` across backends Adds `control-flow/io-sleep/io-sleep.temper.md` to the functional test suite, verifying that `sleep()` from `std/io` works correctly: - Sleep returns and execution continues after `await` - Multiple sequential sleeps work - Zero-ms sleep resolves immediately - Sleep interleaved with computation produces correct results Uses short delays (5-10ms) to avoid slowing the test suite. Passes on: JS, Python, Lua, Java 17, C#. Skipped on Rust (`@Ignore`) because the Rust functional test infrastructure only links `temper-core`, not `temper-std`, so `import("std/io")` produces an unresolved crate at cargo build time. The Rust `sleep` implementation itself works (verified manually via the snake game with `cargo run`). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
97a2086 be-cpp: unsigned right shifting support Signed-off-by: Mike Samuel <mikesamuel@gmail.com>
6707857 fix test golden from adding int.js as a supporting file Signed-off-by: Mike Samuel <mikesamuel@gmail.com>
4519f5d lexer: ensure we do not reset the content line hint inside an interpolation Signed-off-by: Mike Samuel <mikesamuel@gmail.com>
86d4ee7 regenerated docs and fixed a test Signed-off-by: Mike Samuel <mikesamuel@gmail.com>
a3fa212 specialize lua bitops so we can mask shift distances Signed-off-by: Mike Samuel <mikesamuel@gmail.com>
6d734c4 be-lua: number formatting with base Signed-off-by: Mike Samuel <mikesamuel@gmail.com>
4bc8ccc Avoid parse fail on old luas Signed-off-by: Tom <tom@contextfree.info>
133c620 feat: add std/keyboard module with nextKeypress() across 6 backends New std/keyboard module separate from std/io, providing single-keypress input for interactive programs like the snake game. API: nextKeypress(): Promise<String?> returns key names as strings ("a", "ArrowUp", "Enter", "Escape", etc.) or null on EOF. Backend implementations: - Java: stty raw mode (Unix) + PowerShell ReadKey (Windows) - JavaScript: Node setRawMode with escape sequence parsing - Python: tty/termios (Unix) + msvcrt (Windows) - Lua: cooperative scheduler with poll_char and PROMISE_KEYPRESS state - C#: Console.ReadKey with ConsoleKey enum mapping - Rust: crossterm crate for cross-platform keypress reading Windows support on 5/6 backends (Lua is Unix-only for now). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Robert Grayson <bobbbygrayson+github@gmail.com>
92fd175 lexer: Lex new multi-line string context. This commit enables the new margin character based syntax. It removes support for `{:...:}` lines, and recognizes `:` and `~` as margin characters where the former allows for open tokens like the others. This commit adds TokenType info when managing the delimiter stack so that we can make clear distinctions between `"` as a left delimiter, a right delimiter, or a margin character. It also reworks regex literal syntax to use left and right delimiters to avoid ambiguity in later passes with `/foo/i` as a quoted string segment on a margin line. This commit also simplifies TokenClusterTest to, instead of emulating the delimiter stack and saved element stack, to instead interrogate the Lexer using internal `*ForDebug` methods. The nesting levels of synthesized tokens look less pretty, but it matches what updateTokenClusters and popDelimiterStack actually do. Signed-off-by: Mike Samuel <mikesamuel@gmail.com>
9c68c81 release-notes: breaking change warning re string syntax. Signed-off-by: Mike Samuel <mikesamuel@gmail.com>
6d83930 Define missing shift and bitwise operators (#375) Previously, Temper was missing some bit-twiddling operators: - Shift operators: `<<`, `>>`, and `>>>`. - Bitwise negation: `~` - Exclusive or: `^` In keeping with JavaScript for syntactic choices, this commit defines these operators with that punctuation. Unlike JavaScript, our semantics are the same as C-like languages that do not have undefined behaviour on over/under flow, e.g. Java and C#. This commit also defines a functional test that exercises these operators. Some bugs were flushed out as a result: - C#'s *System.Text.Convert.ToString* when used to format numbers as non-decimal, formats as if cast to unsigned: no `-` sign for negative numbers. - be-js does not always render *Int64*s as bigints. - Lua's number formatting ran into trouble with *math.mininteger* because of the 2's complement fixed point around negation with the smallest representible integer. ## Caveat ~~Lua, Rust, and C++ do not currently pass. This commit will remain as part of a draft PR until they do.~~
6ed791d Avoid parse fail on old luas (#381) - This doesn't actually fix the int shifty funtest on old luas, but from manual testing, I *can* run the int limits funtest without crashing on both luajit and lua 5.2 (but not 5.1, but maybe that was an existing issue?) with these changes. - Before these changes, old luas don't run at all for any funtest - Here's the error on plain 5.1: ```lua C:\Users\tjpal\Apps\lua-5.1.5_Win64_bin\lua5.1.exe: .\temper-core/intold.lua:38: attempt to call field 'trunc' (a nil value) stack traceback: .\temper-core/intold.lua:38: in function 'rshift' .\temper-core/intold.lua:64: in function 'int32_mul_parts' .\temper-core/intold.lua:199: in function 'int32_mul' .\work\init.lua:10: in main chunk [C]: ? ```
b74f258 Merge branch 'main' into new-multiline-string-syntax Signed-off-by: Mike Samuel <mikesamuel@gmail.com>
4d4c3d0 fix: simplify readLine to actual line reading across all 6 backends Address PR review feedback: readLine was doing raw TTY single-keypress reading instead of actual line reading. This simplifies all backends to do buffered line input, removes all process-exit calls (System.exit, process.exit, os.kill, Environment.Exit), fixes the Java completeExceptionally bug, and reverts the incidental net8.0 bump in the C# csproj back to net6.0. The keyboard/keypress functionality will be provided by a separate std/keyboard module. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Robert Grayson <bobbbygrayson+github@gmail.com>
75ad3c0 parse: fix OPP test cases for new regex/mq-string syntax and lexing conventions Signed-off-by: Mike Samuel <mikesamuel@gmail.com>
769ab8f fix: make all 6 backends run the snake game correctly Lua: - Replace temper.TODO stub with cooperative coroutine scheduler - async {} now compiles to temper.async_launch() (was "TODO") - LuaTranslator emits temper.run_scheduler() after top-level code - Non-blocking IO: sleep uses deadline-based promises, readLine uses stty min 0 time 0 for polling - Round-robin scheduler drives multiple async blocks cooperatively Rust: - Fix missing temper-std dependency in generated Cargo.toml - Connected functions (stdSleep, stdReadLine) reference temper_std:: paths but bypassed the import-based dependency scan - RustTranslator now tracks usedSupportFunctionPaths - RustBackend scans these after translation to inject temper-std dep with correct features - Also fixes missing temper_std::init() in generated lib.rs - Add raw terminal mode for single-keypress input Java: - Fix waitUntilTasksComplete() 10-second hard timeout - Now loops until ForkJoinPool is truly quiescent - Add raw terminal mode via stty for single-keypress input C#: - Update target framework from net6.0 to net8.0 (current LTS) - Namespace-qualify OrderedDictionary and AsReadOnly in RegexSupport.cs to avoid conflicts with System.Collections.Generic.OrderedDictionary introduced in .NET 9+ - Add single-keypress input via Console.ReadKey Python: - Add raw terminal mode for single-keypress input via tty/termios Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2062ade be-rust: use wrapping versions of shift on Rust to avoid panics on over/under-flow Signed-off-by: Mike Samuel <mikesamuel@gmail.com>
4d44c8f be-lua: debug shift operator wrappers Signed-off-by: Mike Samuel <mikesamuel@gmail.com>
770a408 use C++-11 compatible version of make_unsigned, and use the right g++ on MacOS Signed-off-by: Mike Samuel <mikesamuel@gmail.com>
49f3e38 lexer: margin character cleanup This is in preparation for rolling out new string syntax. Signed-off-by: Mike Samuel <mikesamuel@gmail.com>
8b168f6 be-lua: implement intold shifts based on div/mult by pow(2) Signed-off-by: Mike Samuel <mikesamuel@gmail.com>
1d309d5 release note for bitwise operators Signed-off-by: Mike Samuel <mikesamuel@gmail.com>
88829e3 Implements new multiline string syntax (#380) See the documentation changes for the exact decisions. This implements complex string expressions based on margin characters instead of `{: ... :}` around statement fragments as discussed at https://hackmd.io/gwCoPP9xSPS-Q2nUsNBYaw
70b29ea TokenSourceAdapter works with margin characters Signed-off-by: Mike Samuel <mikesamuel@gmail.com>
ac4ef18 feat: add std/io module with sleep() and readLine() across 6 backends This commit adds two new @connected primitives to Temper's standard library — sleep(ms) and readLine() — wired across all six compilation backends: JavaScript, Python, Lua, Rust, Java, and C#. These are the first I/O primitives in Temper that enable interactive, real-time programs to be written entirely in the Temper language without any host-language wrapper scripts. The motivation: a snake game written in pure Temper needed a game loop that ticks every 200ms. Previously, Temper had no way to pause execution or read user input. The only I/O primitive was console.log(). Programs could compute and print, but could not wait or listen. This meant any interactive program required a host-language wrapper (a Node.js script, a Python script, etc.) to drive the game loop with setTimeout or time.sleep, calling into the compiled Temper module from outside. With sleep() and readLine(), a Temper program can now do this: let {sleep} = import("std/io"); async { (): GeneratorResult<Empty> extends GeneratorFn => do { var game = newGame(20, 10, 42); while (game.status is Playing) { game = tick(game); console.log(render(game)); await sleep(200); } } orelse void; } That code compiles and runs identically on JS, Lua, Rust, Java, and Python (via temper run). No wrapper. No FFI. One source, six targets. --- THE ARCHITECTURE --- Temper's @connected decorator system is the bridge between portable Temper code and backend-specific native implementations. A connected function has a Temper declaration with a panic() body that is never executed — the compiler intercepts the call and routes it to a native implementation registered in the backend's SupportNetwork. The wiring for each connected function follows a 4-layer pattern: 1. Temper declaration: @connected("key") in a .temper.md file 2. Kotlin SupportNetwork: registers the key in the backend compiler 3. Runtime implementation: actual native code (.js, .py, .lua, .rs, etc.) 4. Resource registration: tells the build system to bundle the file This commit touches all four layers for all six backends. --- THE TEMPER DECLARATION (frontend) --- A new std/io module is created at: frontend/.../std/io/io.temper.md It declares two functions: @connected("stdSleep") export let sleep(ms: Int): Promise<Empty> { panic() } @connected("stdReadLine") export let readLine(): Promise<String?> { panic() } Key design decisions: - sleep() returns Promise<Empty>, not Promise<Void>. This is because Temper's await builtin requires the promise's type parameter to extend AnyValue, and Void does not. Empty is a singleton class that does extend AnyValue, so Promise<Empty> is the correct return type for a "returns nothing meaningful" async operation. - readLine() returns Promise<String?>, nullable because EOF returns null. - The bodies are panic() — a convention matching stdNetSend in std/net. The @connected decorator ensures the body is never reached; the backend substitutes its own implementation at compile time. The std config (std/config.temper.md) gains import("./io") to include the new module in the standard library. --- JAVASCRIPT BACKEND (be-js) --- Files changed: - be-js/.../temper-core/io.js (NEW) - be-js/.../temper-core/index.js (export added) - be-js/.../JsBackend.kt (resource registered) - be-js/.../JsSupportNetwork.kt (keys added to supportedAutoConnecteds) The JS backend uses the "auto-connected" pattern: connected keys listed in the supportedAutoConnecteds set are automatically mapped to exported functions whose names follow the connectedKeyToExportedName convention. "stdSleep" maps to an exported function named stdSleep in io.js. The implementation: export function stdSleep(ms) { return new Promise(resolve => setTimeout(() => resolve(empty()), ms)); } This returns a native JS Promise that resolves after ms milliseconds via setTimeout. It resolves with empty() (the Temper Empty singleton) to match the Promise<Empty> return type. readLine() returns a Promise that reads from process.stdin via the 'data' event, or resolves with null if stdin is unavailable (browser). --- PYTHON BACKEND (be-py) --- Files changed: - be-py/.../temper_core/__init__.py (functions added) - be-py/.../PySupportNetwork.kt (PySeparateCode + pyConnections) Python's async model uses concurrent.futures.Future with a ThreadPoolExecutor. The existing _executor and new_unbound_promise() infrastructure (already used by stdNetSend) is reused: def std_sleep(ms): f = new_unbound_promise() def _do_sleep(): time.sleep(ms / 1000.0) f.set_result(None) _executor.submit(_do_sleep) return f The sleep happens on a worker thread; the Future resolves when done. The main thread's generator-based coroutine system picks up the resolution via the existing _step_async_coro machinery. Python programs run via `temper run --library snake -b py`, which generates an entry point that calls await_safe_to_exit() to keep the process alive until all async tasks complete. --- LUA BACKEND (be-lua) --- Files changed: - be-lua/.../temper-core/init.lua (functions + async stub added) Lua is the most interesting case. It has no Promises, no event loop, and no async/await. The Lua translator compiles: - async { ... } → temper.TODO(generatorFactory) - await expr → expr:await() Previously, temper.TODO was undefined (hitting the __index metamethod fallback which errors with "bad connected key: TODO"). This commit adds a minimal stub: function temper.TODO(generatorFactory) local gen = generatorFactory() local co = gen() end This creates the generator and steps it once via coroutine.wrap(), which runs the entire body synchronously (since all awaited operations complete immediately in Lua). For sleep and readLine, the functions are synchronous and return a table with an :await() method so the compiled await translation works: local function make_resolved(value) return { await = function(self) return value end } end function temper.stdsleep(ms) local sec = ms / 1000 local ok, socket = pcall(require, "socket") if ok then socket.sleep(sec) else os.execute("sleep " .. string.format("%.3f", sec)) end return make_resolved(nil) end The sleep implementation tries LuaSocket first (sub-second precision), falling back to os.execute("sleep ...") on systems without it. The naming convention: the default else clause in LuaSupportNetwork's translateConnectedReference converts "stdSleep" to "stdsleep" via .replace("::", "_").lowercase(), which matches temper.stdsleep(). No Kotlin changes needed for Lua. --- RUST BACKEND (be-rust) --- Files changed: - be-rust/.../std/io/support.rs (NEW) - be-rust/.../RustBackend.kt (feature + stdSupportNeeders) - be-rust/.../RustSupportNetwork.kt (FunctionCall entries) Rust uses a custom async runtime (not tokio) based on Promise<T>, PromiseBuilder<T>, and SafeGenerator<T>. The pattern matches stdNetSend exactly: create a PromiseBuilder, spawn async work via run_async(), complete the promise from the worker. pub fn std_sleep(ms: i32) -> Promise<()> { let pb = PromiseBuilder::new(); let promise = pb.promise(); crate::run_async(Arc::new(move || { let pb = pb.clone(); SafeGenerator::from_fn(Arc::new(move |_| { std::thread::sleep(Duration::from_millis(ms as u64)); pb.complete(()); None })) })); promise } The connected reference uses full crate paths ("temper_std::io::std_sleep") because the function lives in the std crate but is called from user crates. The "io" feature is added to stdSupportNeeders and the generated Cargo.toml as io = [] (no external dependencies — only std library). --- JAVA BACKEND (be-java) --- Files changed: - be-java/.../temper/core/Core.java (methods added) - be-java/.../JavaSupportNetwork.kt (separateCode + connections) - be-java/.../StandardNames.kt (qualified names) Java maps Temper Promises to CompletableFuture<T>. The stdSleep implementation runs Thread.sleep on the ForkJoinPool: public static CompletableFuture<Optional<? super Object>> stdSleep(int ms) { CompletableFuture<Optional<? super Object>> future = new CompletableFuture<>(); ForkJoinPool.commonPool().execute(() -> { Thread.sleep(ms); future.complete(Optional.empty()); }); return future; } The return type is CompletableFuture<Optional<? super Object>> because Temper's Empty type maps to Tuple<object?> (via the connectedTypes map), and the generated Java code declares the variable as such. --- C# BACKEND (be-csharp) --- Files changed: - be-csharp/.../std/Io/IoSupport.cs (NEW) - be-csharp/.../CSharpBackend.kt (resource registered) - be-csharp/.../CSharpSupportNetwork.kt (StaticCall entries) - be-csharp/.../StandardNames.kt (namespace + member names) C# has native async/await with Task<T>, making this the most natural fit. The implementation uses Task.Delay for non-blocking sleep: public static async Task<Tuple<object?>> StdSleep(int ms) { await Task.Delay(ms); return Tuple.Create<object?>(null); } The return type is Task<Tuple<object?>> because C# maps Temper's Empty to System.Tuple (via the connectedTypes map entry "Empty" -> systemTuple). --- VERIFICATION --- All backends compile. Tested with a snake game (18 unit tests passing on JS backend, game loop running on JS, Lua, Rust, Java, and Python): JS: node temper.out/js/snake/index.js Lua: cd temper.out/lua && lua snake/init.lua Rust: cd temper.out/rust/snake && cargo run Java: javac + java -cp build snake.SnakeMain Python: temper run --library snake -b py C#: dotnet build succeeds (needs net6.0 runtime to execute) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1650f8b Define missing shift and bitwise operators Previously, Temper was missing some bit-twiddling operators: - Shift operators: `<<`, `>>`, and `>>>`. - Bitwise negation: `~` - Exclusive or: `^` In keeping with JavaScript for syntactic choices, this commit defines these operators with that punctuation. Unlike JavaScript, our semantics are the same as C-like languages that do not have undefined behaviour on over/under flow, e.g. Java and C#. This commit also defines a functional test that exercises these operators. Some bugs were flushed out as a result: - C#'s *System.Text.Convert.ToString* when used to format numbers as non-decimal, formats as if cast to unsigned: no `-` sign for negative numbers. - be-js does not always render *Int64*s as bigints. - Lua's number formatting ran into trouble with *math.mininteger* because of the 2's complement fixed point around negation with the smallest representible integer. ## Caveat Lua, Rust, and C++ do not currently pass. This commit will remain as part of a draft PR until they do. Signed-off-by: Mike Samuel <mikesamuel@gmail.com>
085161f chore: add stdReadLine and stdSleep to connected list in README Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Robert Grayson <bobbbygrayson+github@gmail.com>
+372-136 temperlang/temper/do-crimes-to-play-snake Mar 19 22:05
b11d05d chore: formatting fixes and update connected list in README Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
e25c48b fix: add Windows support to Java nextKeypress via PowerShell Split stdNextKeypress into Unix (stty) and Windows (PowerShell ReadKey) paths. Uses [Console]::ReadKey($true) to read a single key without echo, parsing the VirtualKeyCode for arrow keys and special keys. No external dependencies, works with Java 8. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
3ca9007 feat: add std/keyboard module with nextKeypress() across 6 backends New std/keyboard module separate from std/io, providing single-keypress input for interactive programs like the snake game. API: nextKeypress(): Promise<String?> returns key names as strings ("a", "ArrowUp", "Enter", "Escape", etc.) or null on EOF. Backend implementations: - Java: stty raw mode with arrow key escape sequence parsing - JavaScript: Node setRawMode with escape sequence parsing - Python: tty/termios (Unix) + msvcrt (Windows) - Lua: cooperative scheduler with poll_char and PROMISE_KEYPRESS state - C#: Console.ReadKey with ConsoleKey enum mapping - Rust: crossterm crate for cross-platform keypress reading Windows support on 5/6 backends (Lua is Unix-only for now). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ac9da05 fix: simplify readLine to actual line reading across all 6 backends Address PR review feedback: readLine was doing raw TTY single-keypress reading instead of actual line reading. This simplifies all backends to do buffered line input, removes all process-exit calls (System.exit, process.exit, os.kill, Environment.Exit), fixes the Java completeExceptionally bug, and reverts the incidental net8.0 bump in the C# csproj back to net6.0. The keyboard/keypress functionality will be provided by a separate std/keyboard module. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
+87-15 notactuallytreyanastasio/alloy-js/main Mar 19 13:02
9af4067 Update from alloy 6199c7d47d9ce8aedad73dbb03d4d43c358fc83a
+86-14 notactuallytreyanastasio/alloy-js-app/main Mar 19 13:02
4f1b69b Update ORM vendor from 9af4067dd2ab397fe2c2087a33d7a9a1b0f513e3
+314-0 notactuallytreyanastasio/alloy/main Mar 19 12:56
6199c7d add some notes
+576-2363 notactuallytreyanastasio/deciduous/main Mar 19 02:17
862218f Cleanup phase 1: dead code removal + hook fix (#193) * cleanup: remove diff.rs and git_guard/ — Phase 1 dead code deletion Remove two subsystems identified as dead/redundant code: - diff.rs (607 LOC): Legacy patch-based sync superseded by events.rs - git_guard/ (1,337 LOC): Redundant git safety — Claude Code has built-in protections Also removes ~285 LOC of Diff command handling from main.rs and re-exports from lib.rs. Adds 24 new regression tests covering core functionality: node CRUD, delete cascading, unlink, show/JSON, prompt updates, status transitions, complex graph traversal, DOT filtering, backup, empty DB edge cases, branch filtering, writeup, all edge types, revisit nodes, and supersede. Net: -2,266 lines deleted, +448 lines added (regression tests) Closes #184, closes #185 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(hooks): make post-commit reminder advisory, add tester prompt - Changed post-commit-reminder.sh from exit 2 (blocking error) to exit 0 with a one-line advisory reminder - Added tester/prompt.md for the testing framework exercise Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * release: v0.13.14 - advisory post-commit hook Post-commit reminder hook now uses exit 0 (advisory) instead of exit 2 (blocking). Applies to both Claude Code and Windsurf templates. Existing projects get the fix via `deciduous update`. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(update): prompt user to commit after deciduous update After `deciduous update` overwrites tooling files, print the exact git add + git commit commands needed to lock in the new configuration. Shows the right paths for whichever assistants are installed (Claude Code, OpenCode, Windsurf). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(ci): remove Elixir format workflow, run cargo fmt - Deleted .github/workflows/format.yml (Elixir mix format on a Rust project) - Ran cargo fmt to fix formatting nits in main.rs and cli_integration.rs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
+33-62 notactuallytreyanastasio/deciduous/cleanup Mar 19 02:16
ca8271c fix(ci): remove Elixir format workflow, run cargo fmt - Deleted .github/workflows/format.yml (Elixir mix format on a Rust project) - Ran cargo fmt to fix formatting nits in main.rs and cli_integration.rs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
+23-0 notactuallytreyanastasio/deciduous/cleanup Mar 19 01:52
c8ac7b5 feat(update): prompt user to commit after deciduous update After `deciduous update` overwrites tooling files, print the exact git add + git commit commands needed to lock in the new configuration. Shows the right paths for whichever assistants are installed (Claude Code, OpenCode, Windsurf). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
+21-39 notactuallytreyanastasio/deciduous/cleanup Mar 19 01:27
5a4b7c3 release: v0.13.14 - advisory post-commit hook Post-commit reminder hook now uses exit 0 (advisory) instead of exit 2 (blocking). Applies to both Claude Code and Windsurf templates. Existing projects get the fix via `deciduous update`. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
+74-19 notactuallytreyanastasio/deciduous/cleanup Mar 19 01:19
f500b27 fix(hooks): make post-commit reminder advisory, add tester prompt - Changed post-commit-reminder.sh from exit 2 (blocking error) to exit 0 with a one-line advisory reminder - Added tester/prompt.md for the testing framework exercise Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
+862-19 notactuallytreyanastasio/deciduous/cleanup Mar 19 01:05
741a3f6 feat(tester): scaffold Ruby testing framework with hash-based test cases Core assertions (assert, refute, assert_equal, assert_raise), DSL with describe/test/setup/teardown blocks, and a runner with colored output. Deliberately using plain hashes for test case representation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fccd25d feat(tester): scaffold Ruby testing framework with nested hash tree Core assertions: assert, refute, assert_equal, refute_equal, assert_raise, assert_match, assert_includes. DSL with describe/test/setup/teardown blocks using nested hash tree structure (deliberate over-engineering — will revisit). Runner with colored output and recursive tree walking. 19 self-hosting tests passing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
93bea7f refactor(tester): replace nested hash tree with flat array + setup inheritance Supersedes the over-engineered recursive hash tree approach. DSL now collects tests into a flat array with path-prefix names. Setup/teardown inherited via stack — parent setups propagate to children. Runner simplified: no more recursive tree walking. Fixed Ruby gotcha: [nil].any? returns false, use .empty? instead. 30 tests passing across 3 suites. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
18949d9 feat(tester): add multi-suite runner and split tests into separate files Runner.run_all aggregates results across multiple suites. Split smoke_test into assertion_test, mailbox_test, dsl_test. run_all.rb as entry point. 28 tests across 3 suites. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1dfa15e fix(hooks): make post-commit reminder advisory instead of blocking The PostToolUse:Bash hook was using exit 2 which Claude Code treats as an error, generating spurious hook errors on every git commit. Changed to exit 0 with a one-line stdout reminder — informational, not blocking. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ad799d2 feat(tester): add assert_receive/refute_receive with mailbox pattern Pure data mailbox: send messages, receive by exact match or regex. assert_receive pulls matching message from queue. refute_receive verifies no match exists. 25 self-hosting tests passing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
+12-4 temperlang/temper/main Mar 18 18:08
a295294 fix: reset for-loop variable on each iteration in Rust async state machine (#379) Fixes #378 When a `for (var i = 0; ...)` loop is inside a while loop and the for body contains an `await`, the Rust backend's coroutine-to-state-machine transformation extracts the variable declaration to a persistent struct field but drops the initializer from the case body. This means `i = 0` only runs once (at generator creation), so the inner loop silently never executes after the first outer iteration. The fix: don't extract the initializer for ValueLeaf (simple value) declarations. Extract just the declaration to persistent storage, and leave the `i = 0` assignment in the state machine case body so it re-executes every time the case is entered. Before: persistent: `var i: Int = 0` (runs once) case body: (nothing) After: persistent: `var i: Int` (declaration only) case body: `i = 0` (runs on each case entry) This is safe for non-loop declarations too — the assignment just runs once when the case is entered, same as before. Minimal reproduction (pure Temper, no deps): ``` while (outerCount < 3) { outerCount = outerCount + 1; for (var i = 0; i < items.length; ++i) { totalLoopBodyRuns = totalLoopBodyRuns + 1; await resolved(); } } ``` JS (correct): total loop body runs: 9 Rust before: total loop body runs: 3 Rust after: total loop body runs: 9 All existing be-rust and be-js tests pass.
+44-40 notactuallytreyanastasio/deciduous/main Mar 18 05:23
57315ca release: v0.13.13 - sort narratives by most recent activity Narratives in the web viewer are now sorted by most recent activity (latest node timestamp) instead of tree size, across all modes: goals, significant, branches, and hubs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
+12-6 notactuallytreyanastasio/deciduous/main Mar 17 23:57
9925de1 release: v0.13.12 - web viewer defaults to showing all goals Changed default narrative mode from 'significant' (10+ nodes only) to 'goals' (all goals) so the viewer page shows everything by default. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
+171-183 notactuallytreyanastasio/deciduous/main Mar 17 23:28
45905ab release: v0.13.11 - always-on version check with semver-aware messaging Version checking is now always-on (removed opt-in toggle). Notifications vary by severity: patch updates get a quiet one-liner, minor/major updates get a prominent banner encouraging upgrade. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
+17-2 notactuallytreyanastasio/deciduous/main Mar 17 20:38
b2246e6 release: v0.13.10 - auto-update hook and Q&A copy markdown button Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
+11-2 notactuallytreyanastasio/deciduous/main Mar 17 20:37
95d0922 release: v0.13.10 - auto-update hook and Q&A copy markdown button Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
+543-20 notactuallytreyanastasio/deciduous/main Mar 17 20:31
ed3c05d feat: opt-in auto-update version check hook (#179) * feat: opt-in auto-update hook system - Add version-check hook for Claude Code, OpenCode, and Windsurf - Hook checks crates.io once per 24h (rate-limited, 3s timeout) - Opt-in via config: `deciduous auto-update on/off` - Non-blocking (exit 0) - AI assistant informs user conversationally - Results cached in .deciduous/.latest_version - Disabled by default (auto_check = false in config.toml) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * default auto_check to true, add --no-auto-update flag to init Auto version checking is now on by default for new projects. Users can opt out during init with --no-auto-update, or toggle later with deciduous auto-update off. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: document auto-update feature across all docs and tutorials - README.md: auto-update section, commands reference, hooks table - CLAUDE.md: CLI commands table, quick reference, session checklist - docs/QUICK_REFERENCE.md: session recovery, file locations table - docs/tutorial/reference: command table, detailed command docs, config - docs/tutorial/getting-started: init config description - src/init/templates.rs: CLAUDE_MD_SECTION session checklist - src/opencode.rs: CLI commands, session checklists (both locations) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * format on push to not-main --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
+64-41 notactuallytreyanastasio/deciduous/auto-update-hook Mar 17 20:31
e5a7a10 Merge branch 'main' into auto-update-hook
+12-4 temperlang/temper/fix/rust-for-loop-var-reset Mar 16 14:57
52c431a fix: reset for-loop variable on each iteration in Rust async state machine Fixes #378 When a `for (var i = 0; ...)` loop is inside a while loop and the for body contains an `await`, the Rust backend's coroutine-to-state-machine transformation extracts the variable declaration to a persistent struct field but drops the initializer from the case body. This means `i = 0` only runs once (at generator creation), so the inner loop silently never executes after the first outer iteration. The fix: don't extract the initializer for ValueLeaf (simple value) declarations. Extract just the declaration to persistent storage, and leave the `i = 0` assignment in the state machine case body so it re-executes every time the case is entered. Before: persistent: var i: Int = 0 (runs once) case body: (nothing) After: persistent: var i: Int (declaration only) case body: i = 0 (runs on each case entry) This is safe for non-loop declarations too — the assignment just runs once when the case is entered, same as before. Minimal reproduction (pure Temper, no deps): while (outerCount < 3) { outerCount = outerCount + 1; for (var i = 0; i < items.length; ++i) { totalLoopBodyRuns = totalLoopBodyRuns + 1; await resolved(); } } JS (correct): total loop body runs: 9 Rust before: total loop body runs: 3 Rust after: total loop body runs: 9 All existing be-rust and be-js tests pass. Signed-off-by: Robert Grayson <bobbbygrayson+github@gmail.com>
+12-4 temperlang/temper/fix/rust-for-loop-var-reset Mar 16 13:55
48c6bf8 fix: reset for-loop variable on each iteration in Rust async state machine Fixes #378 When a `for (var i = 0; ...)` loop is inside a while loop and the for body contains an `await`, the Rust backend's coroutine-to-state-machine transformation extracts the variable declaration to a persistent struct field but drops the initializer from the case body. This means `i = 0` only runs once (at generator creation), so the inner loop silently never executes after the first outer iteration. The fix: don't extract the initializer for ValueLeaf (simple value) declarations. Extract just the declaration to persistent storage, and leave the `i = 0` assignment in the state machine case body so it re-executes every time the case is entered. Before: persistent: var i: Int = 0 (runs once) case body: (nothing) After: persistent: var i: Int (declaration only) case body: i = 0 (runs on each case entry) This is safe for non-loop declarations too — the assignment just runs once when the case is entered, same as before. Minimal reproduction (pure Temper, no deps): while (outerCount < 3) { outerCount = outerCount + 1; for (var i = 0; i < items.length; ++i) { totalLoopBodyRuns = totalLoopBodyRuns + 1; await resolved(); } } JS (correct): total loop body runs: 9 Rust before: total loop body runs: 3 Rust after: total loop body runs: 9 All existing be-rust and be-js tests pass. Signed-off-by: Robert Grayson <bobbbygrayson+github@gmail.com>
+12-4 temperlang/temper/fix/rust-for-loop-var-reset Mar 16 13:54
3683385 fix: reset for-loop variable on each iteration in Rust async state machine Fixes #378 When a `for (var i = 0; ...)` loop is inside a while loop and the for body contains an `await`, the Rust backend's coroutine-to-state-machine transformation extracts the variable declaration to a persistent struct field but drops the initializer from the case body. This means `i = 0` only runs once (at generator creation), so the inner loop silently never executes after the first outer iteration. The fix: don't extract the initializer for ValueLeaf (simple value) declarations. Extract just the declaration to persistent storage, and leave the `i = 0` assignment in the state machine case body so it re-executes every time the case is entered. Before: persistent: var i: Int = 0 (runs once) case body: (nothing) After: persistent: var i: Int (declaration only) case body: i = 0 (runs on each case entry) This is safe for non-loop declarations too — the assignment just runs once when the case is entered, same as before. Minimal reproduction (pure Temper, no deps): while (outerCount < 3) { outerCount = outerCount + 1; for (var i = 0; i < items.length; ++i) { totalLoopBodyRuns = totalLoopBodyRuns + 1; await resolved(); } } JS (correct): total loop body runs: 9 Rust before: total loop body runs: 3 Rust after: total loop body runs: 9 All existing be-rust and be-js tests pass. Signed-off-by: Robert Grayson <bobbbygrayson+github@gmail.com>
+12-4 temperlang/temper/fix/rust-for-loop-var-reset Mar 16 12:28
a2270c2 fix: reset for-loop variable on each iteration in Rust async state machine Fixes #378 When a `for (var i = 0; ...)` loop is inside a while loop and the for body contains an `await`, the Rust backend's coroutine-to-state-machine transformation extracts the variable declaration to a persistent struct field but drops the initializer from the case body. This means `i = 0` only runs once (at generator creation), so the inner loop silently never executes after the first outer iteration. The fix: don't extract the initializer for ValueLeaf (simple value) declarations. Extract just the declaration to persistent storage, and leave the `i = 0` assignment in the state machine case body so it re-executes every time the case is entered. Before: persistent: var i: Int = 0 (runs once) case body: (nothing) After: persistent: var i: Int (declaration only) case body: i = 0 (runs on each case entry) This is safe for non-loop declarations too — the assignment just runs once when the case is entered, same as before. Minimal reproduction (pure Temper, no deps): while (outerCount < 3) { outerCount = outerCount + 1; for (var i = 0; i < items.length; ++i) { totalLoopBodyRuns = totalLoopBodyRuns + 1; await resolved(); } } JS (correct): total loop body runs: 9 Rust before: total loop body runs: 3 Rust after: total loop body runs: 9 All existing be-rust and be-js tests pass.
+2177-1865 notactuallytreyanastasio/snake-rust/main Mar 16 11:07
f97f776 Update from temper_snake 43c00a9c8607047854c74f427095d22fb00b36f9
+1265-1213 notactuallytreyanastasio/snake-lua/main Mar 16 11:07
912b30c Update from temper_snake 43c00a9c8607047854c74f427095d22fb00b36f9
+1600-1486 notactuallytreyanastasio/snake-js/main Mar 16 11:07
028958b Update from temper_snake 43c00a9c8607047854c74f427095d22fb00b36f9
+1583-1534 notactuallytreyanastasio/snake-python/main Mar 16 11:07
bdd12df Update from temper_snake 43c00a9c8607047854c74f427095d22fb00b36f9
+813-701 notactuallytreyanastasio/snake-csharp/main Mar 16 11:07
afcbe54 Update from temper_snake 43c00a9c8607047854c74f427095d22fb00b36f9
+1041-793 notactuallytreyanastasio/snake-java/main Mar 16 11:07
01210ed Update from temper_snake 43c00a9c8607047854c74f427095d22fb00b36f9
+2639-59 notactuallytreyanastasio/temper_snake/main Mar 16 11:03
43c00a9 add deciduous
4dc6928 feat: bots randomly pick JS, Rust, or Python compiled client Each AI bot spawns a random compiled Temper client (JS, Rust, or Python) as a subprocess and controls it by piping directions to stdin and reading rendered frames from stdout. The bot wrapper: 1. Picks a random backend (or accepts one as CLI arg) 2. Spawns the compiled client as a child process 3. Reads ASCII frames from the child's stdout 4. Sends frames to Ollama with personality prompt 5. Writes the LLM's chosen direction to the child's stdin This means each bot is genuinely running a different language's compiled output — a Rust binary, a Node.js script, or a Python program — all connecting to the same server via WebSocket. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
d6f31aa feat: add AI snake bots powered by local LLMs via Ollama Each bot connects as a regular WebSocket client — the server can't tell it apart from a human. The bot receives rendered ASCII frames, strips ANSI escapes, sends the board to a local Ollama model with a personality-specific system prompt, parses the response for a direction (u/d/l/r), and sends it to the server. 6 personalities: greedy (chase food), cautious (survive), aggressive (hunt other snakes), chaotic (unpredictable), hunter (target the leader), wall_hugger (patrol edges). Usage: node bot/snake-bot.js greedy mistral-small bash bot/swarm.sh llama3.2:3b # launches 4 bots at once All bots share one model in Ollama (~2GB for 3B params). Each bot decides every ~1-2s. Between decisions, the snake keeps moving in its current direction — gives a natural "thinking" feel. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
be3cf48 feat: add spectator mode and join/spectate handshake protocol Clients now send a handshake message on connect: - "join" → player (spawns a snake) - "spectate" → spectator (receives frames, no snake) Server waits for the first message to decide. Spectators get added to the broadcast list but no PlayerSnake is created. They see the full board with all players and bots but don't affect the game. New file: bot/spectate.js — minimal spectator client. Updated: client sends "join", bot sends "join", spectate sends "spectate". Usage: node bot/spectate.js # watch the game node bot/spectate.js ws://host:8080 # watch a remote game Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
7de9814 fix: nullable string comparison in spectator handshake for Rust backend The Rust codegen can't compare String? directly to a string literal. Use `is String` type check first, then compare the narrowed value. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
0329d61 feat: randomize snake spawn positions with edge buffer Spawn positions are now PRNG-driven instead of cycling through 4 fixed quarter positions. Each snake gets a random position at least 5 cells from any edge (so it has room to react before hitting a wall) and a random direction. The seed is mixed with the player index for deterministic but spread-out placement. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
+3289-570 notactuallytreyanastasio/snake-java/main Mar 15 23:11
125731a Update from temper_snake c8f06c7238f126a9988baa78bfcb5e103626b476
+9372-5725 notactuallytreyanastasio/snake-js/main Mar 15 23:11
2086e05 Update from temper_snake c8f06c7238f126a9988baa78bfcb5e103626b476
+2274-996 notactuallytreyanastasio/snake-python/main Mar 15 23:11
f5e407e Update from temper_snake c8f06c7238f126a9988baa78bfcb5e103626b476
+3771-680 notactuallytreyanastasio/snake-rust/main Mar 15 23:11
8f3106b Update from temper_snake c8f06c7238f126a9988baa78bfcb5e103626b476
+2999-600 notactuallytreyanastasio/snake-csharp/main Mar 15 23:11
66ec778 Update from temper_snake c8f06c7238f126a9988baa78bfcb5e103626b476
+1872-503 notactuallytreyanastasio/snake-lua/main Mar 15 23:11
f2de24b Update from temper_snake c8f06c7238f126a9988baa78bfcb5e103626b476
+1090-11 notactuallytreyanastasio/temper_snake/main Mar 15 23:07
c8f06c7 server frames correction
aaf8625 feat: add multiplayer snake over WebSockets Add networked multiplayer to the snake game. One player starts a server, others connect as clients, and everybody shares the same board. The entire multiplayer stack — game logic, protocol, server, client — is written in Temper and compiles to every backend. This commit covers the game-side changes. The compiler-side changes (adding std/ws and terminal size detection to std/io) live on the `do-more-crimes-to-play-snake-multiplayer` branch in the Temper repo. ## What changed ### Multi-snake game logic (src/snake.temper.md, +437 lines) New types: - `PlayerStatus` (sealed): `Alive` / `Dead` - `PlayerSnake`: id, segments, direction, score, status - `MultiSnakeGame`: width, height, list of snakes, food, rng, tick count New functions: - `newMultiGame(width, height, numPlayers, seed)` — spawns snakes at spread-out positions around the board (quarters, facing inward) - `multiTick(game, directions)` — the core multiplayer tick. Handles: - Per-snake direction changes (rejects opposite) - Wall collision (per snake) - Self collision (per snake) - Head-to-body collision (your head hits another snake's body) - Head-to-head collision (two snakes move to the same cell — both die) - Food eating (first alive snake to reach food gets the point) - Food respawning (avoids all snake segments) - `multiRender(game)` — renders the board with per-player symbols: P0: @/o, P1: #/+, P2: $/~, P3: %/=, P4+: &/. Shows score and alive/dead status per player below the board. - `changePlayerDirection(game, playerId, dir)` — updates one player - `isMultiGameOver(game)` — true when <=1 snake alive (2+ players) - `addPlayer(game, seed)` — adds a snake to a running game - `removePlayer(game, playerId)` — removes a disconnected player - `directionToString` / `stringToDirection` — serialization helpers All single-player code is untouched. The 18 original tests still pass. ### Server (server/server.temper.md) A WebSocket server using the new `std/ws` module. Architecture: - Accept loop (async block): listens on port 8080, accepts connections, calls `addPlayer` to spawn a new snake, then spawns a per-connection recv loop as another async block - Per-connection recv loop (async block per player): reads single-char direction messages (u/d/l/r), calls `changePlayerDirection` - Game loop (async block): ticks every 200ms, builds the directions list from current snake states, calls `multiTick`, broadcasts the rendered frame to all connections via `wsSend` The board size is calculated from the server's terminal dimensions minus a 10-character margin on each axis, using the new `terminalColumns()` and `terminalRows()` from `std/io`. Players can join at any time. The server handles disconnections gracefully (the snake just stops getting direction updates and eventually hits a wall). ### Client (client/client.temper.md) A WebSocket client that connects to the server. Two async blocks: - Input loop: reads w/a/s/d via `readLine()`, maps to single-char direction codes (u/d/l/r), sends via `wsSend` - Recv loop: receives messages via `wsRecv`, prints them directly (the server sends fully-rendered frames as plain text) The client is intentionally dumb. It has no game logic. It sends keypresses and displays whatever the server sends back. ### Protocol Deliberately minimal, no parsing required: - Client → Server: single characters "u", "d", "l", "r" - Server → Client: complete rendered ASCII frames (the output of `multiRender`), sent as-is over the WebSocket No JSON. No message framing. No serialization library. The server renders the board, sends the string, the client prints it. ### Tests (test/snake_test.temper.md, +13 tests) New tests covering: multi-game creation, snake count, alive status, different spawn positions, segment count, both snakes moving on tick, wall collision killing a snake, game-over detection, direction changes, opposite direction rejection, addPlayer, removePlayer, render output, and direction serialization round-trips. Total: 31 tests (18 single-player + 13 multiplayer). All pass. ## How to play # Terminal 1: start server cd temper.out/js && node snake-server/index.js # Terminal 2: player 1 cd temper.out/js && node snake-client/index.js # Terminal 3: player 2 cd temper.out/js && node snake-client/index.js Connect as many players as you want. ## Compiler changes required (separate branch) The Temper compiler gained a new `std/ws` module with 6 @connected functions and 2 opaque types, wired for JS (`ws` npm package) and Rust (`tungstenite` crate). Also `terminalColumns()`/`terminalRows()` in `std/io`. 13 files changed, 554 insertions. Committed on branch `do-more-crimes-to-play-snake-multiplayer` in the Temper repo. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
8f1bc83 docs: detailed report on Rust backend broadcast loop await bug Documents a codegen bug where for-loop variables inside outer loops don't reset when the inner loop awaits a synchronously-resolved Promise. Includes root cause analysis (re-entrant generator.next() from inline on_ready callbacks), the exact state machine case flow, evidence from debug logging, the workaround, and scope analysis. Verified the bug does not reproduce with async-resolved Promises (sleep(0)) — only with @connected functions that call pb.complete() synchronously (like wsSend). The JS backend is unaffected. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
11a0d08 fix: update compiler branch refs to do-more-crimes-to-play-snake-multiplayer CI and build instructions now point to the new Temper compiler branch that includes std/ws (WebSocket) and std/io terminal size detection. Updates the git clone in the GitHub Actions workflow, the generated child repo READMEs, and the manual build instructions in README.md. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
cdcf29d docs: update bug report with minimal repro on Temper main Confirmed the for-loop variable reset bug on Temper main (cb8c3d5) with a pure-Temper reproduction — no std/io, no WebSocket, no external dependencies. Just PromiseBuilder + await inside a for loop inside a while loop. JS: "total loop body runs: 9" — PASS Rust: "total loop body runs: 3" — FAIL Root cause: `for (var i = 0; ...)` compiles the init as a struct field default (runs once at generator creation), not as a per-iteration assignment in the state machine. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ca39530 docs: add JS, Rust, and Python multiplayer setup instructions Complete setup and run instructions for all three backends with server and client commands. Documents mix-and-match interop. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
f6f6f11 get thigns working for rust and stuff
c52a6fc docs: rewrite bug report referencing only Temper main Clean report with no references to snake, WebSocket, or multiplayer code. Minimal reproduction uses only core Temper primitives (PromiseBuilder, async/await, for loop). Tested on main at cb8c3d5. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
38 pushes github.com/notactuallytreyanastasio
phangraphs — Phish 3.0 Jam Analytics
phangraphs
Soul Planet
.458
BATTING AVG
11 JC · 24× played
Avg 14.0m · Peak 46:49 · 🎧 18/24
Longest: 46:49
2021-08-31 · Shoreline Amphitheatre
Most Loved: 30 likes
2021-08-31 · Shoreline Amphitheatre
📊 75% Set 2 · 9/11 JCs from Set 2
Last: 2025-07-12 (251d ago) · avg every 111d
Best year: 2021 — 4/4 JC
🔥 JC Streak: 3 in a row
"Breaks out with some brief funk and quickly evolves into a warmer groove with Pa..."
TAP TO SEE AND PLAY JAMS
Soul Planet .458
TAP TO FLIP BACK
11 jams
2018-07-28 16:08
The Forum · Inglewood, CA
Breaks out with some brief funk and quickly evolves into a warmer groove with Page and Trey trading lead duties before guitar takes over as the jam gallops to a peak and dissolves into "Wingsuit".
2018-08-04 23:40
Verizon Wireless Amphitheatre at Encore Park · Alpharetta, GA
Pivots out of the verses into a grimy jam that briefly picks up speed before devolving into envelope-filtered-Mike-led funk, then blossoms into a warm and relaxed jam full of delicate synths from Page and sympathetic Trey soloing. Things take an anthemic turn at Mike's urging, then appear to be coming to a close, but then gathers up energy once more and builds to a driving peak. But rather than close out, Fish and Page lead the way to another filthy and funky groove, with tinges of 2.0 jamming style, before dying out and > into "ASIHTOS".
2018-11-02 16:56
MGM Grand Garden Arena · Las Vegas, NV
Soaring, anthemic play from Trey combined with gooey bass from Mike highlight this second set opening version. After a mini-peak, the jam eventually fizzles to > "Down With Disease".
2021-08-10 13:27
Hersheypark Stadium · Hershey, PA
The other Summer 2021 "Soul Planet", this bad boy doesn't take long to move into a quicksilver minor-key boil with Trey's gnarly effects taking center stage. Fish picks up the pace and a solid groove emerges, then Trey switches to major-key and the band follows with a powerful bit of hose. Fish then switches up his beat and Page takes center stage, before Trey elbows his way in with some wicked riffing and Fish steers things to more rock-oriented waters. Trey then fires up some stabbing chords before neatly -> into NICU. Inspired, melodious jamming.
2021-08-31 46:49
Shoreline Amphitheatre · Mountain View, CA
Smoothly runs the gamut from dark grooves to a lovely uplifting space to double-time rocking out to a muscular jamming zone where Mike's drill (!) takes center stage to anthemic bliss to something akin to an alien distress signal that could've only come from Summer 2021 to sludgy industrial noise to even faster rocking out to snappy funkiness to a perfect -> into "The Final Hurrah". A cornerstone jam of Summer 2021, and the third longest of their entire career. Yes, you read that right, their entire career.
2021-10-22 18:19
Ak-Chin Pavilion · Phoenix, AZ
"Hmm... this jam is okay, I can imagine many of my friends lapping this up but I'm mostly disintere-SWEET HEAVENLY LORD THIS FINAL PEAK IS AMAZING."
2021-10-31 14:34
MGM Grand Garden Arena · Las Vegas, NV
Deep in the third set of the Halloween show, the band still has enough creative juice for one more killer jam, as Fish kicks up the tempo and Page does some fine electric piano work, before a slower and more contemplative jam emerges. Fish picks the pace back up and the band enters anthemic mode, Trey's funky new filter adding extra spice, then things get darker and nastier almost out of nowhere with a stomping and powerful jam. -> into "Death Don't Hurt Very Long" out of the muck.
2022-08-05 14:51
Atlantic City Beach · Atlantic City, NJ
> from "Axilla (Part II)". Mike's bass is thumping as the jam begins. Trey solos over a cool texture provided by Mike and Fish. A slow tempo gradually picks up steam, possibly because of Fish dropping a "Yeah" sample seemingly out of nowhere. The jam comes to a head around 13:00 with Trey and Mike both heavy in their effects arsenals. It melts into almost nothing before > into a big "Down With Disease".
2023-07-22 14:11
The Pavilion at Star Lake · Burgettstown, PA
Hard-charging out of the gates through 7:15 when it settles into an ethereal transition, then winds up again for a heartening resolution and floats into -> "Twist".
2024-08-04 26:13
Ruoff Music Center · Noblesville, IN
Along with preceding "Ghost", this "Soul Planet" makes up one of the great 1-2 punches of the year as it sets off at a good pace and introduces dial-tone and other wonky effects in the opening minutes before a shift around 8:00. From there it drifts upwards into major bliss and is driven into gorgeously inspirational territory by Trey's coalescing riff for a terrific mid-jam hosing, after which the band briefly gestures towards a return home, only to quickly launch into more locked-in improvisation. With the vibe now turning towards hard-rock, the jam once again gels around Trey's riffage and surges to a head-banging peak, then > "Billy Breathes".
2025-07-12 12:34
North Charleston Coliseum · North Charleston, SC
There's no time wasted in this all killer, no filler jam, which gets especially gnarly after 8:00 when it develops a mean streak and goes screaming through space to a supernova conclusion.
phangraphs Phish 3.0 Jam Analytics
AIM Chat - Terminal
Online (0)
jeff 02:52 AM
What is up y'all
jeff 03:44 AM
Thanks for visiting my website
jeff 03:51 AM
Trying from mobile let’s see what’s up
Uechi Nerd 03:54 AM
Hi, greetings from Planet Crackpot!
jeff 03:55 AM
oh man what's up
jeff 03:56 AM
Hey everyone
Uechi Nerd 03:56 AM
winding down with a beer or three
Uechi Nerd 03:57 AM
desktop version
Uechi Nerd 04:01 AM
Disappointed that War does not lead to actual combined-arms conflict.
jeff 04:01 AM
that would be hard to conjure
jeff 04:02 AM
I am so excited that this works and is a successful combination of windows and old apple lol
Uechi Nerd 04:02 AM
Probably for the best, actually. That shit is very very messy.
Uechi Nerd 04:02 AM
I am intrigued and happy it works!
Uechi Nerd 04:03 AM
I respect the wizardry.
Visitor7804 04:05 AM
this is delightful.
jeff 04:06 AM
hell yeah visitor 7804, this is livin' brother
guy4get 04:07 AM
i've never felt so alive
EarlofVincent 04:09 AM
Commencing experiment in 3....2....
jeff 04:14 AM
1
leah 04:16 AM
hi!
leah 04:16 AM
this is lovely
jeff 04:21 AM
hi! lol I was just like what if I combined Mac and windows and added a flower tree of life and called it my homepage and then smoked some weed and made it happen in an empty mall in Connecticut
B. Droptables 10:51 AM
Always cool to play with your toys.
Visitor1128 08:47 AM
yo!
Visitor1128 08:48 AM
i can barely work my phone. what am i doing here?
jeff 09:04 AM
the phone is not optimized yet but it "kind of works" I am sorry lol
jeff 09:04 AM
you have to pick a username, then it goes to the chat, then if you hit the bottom tabs it'll let you go to the app sections.
Bobdawg 04:43 AM
Hi everybody this is my blog I hope you enjoy it I did some more changes and anyone can write a post here now for me.
dinkleberg 01:45 AM
ALL HAIL TREE OF LIFE
jeff 08:55 PM
hi Hacker News
jeff 04:28 PM
hey there I am not really Jeff
Mal Function 05:34 PM
Hey! Please reveal... how exactly do I actually use losselot on my Mac? I've run the git clone commend in Terminal.app and seem successfully to have installed into a new <losselot> sub-folder in my home folder but now???