SLOPSHOPPER

olo-known-traps

Recognises error output from traps already recorded in OloEngine's memory notes and attaches the recorded fix at the moment the error appears.

newguard
★ 17v0.1.0NOASSERTIONupdated 2026-10-10drsnuggles8/OloEngineBase/.claude/mods/olo-known-traps
A shopper browsing a rack in a slop shop
README

OloEngine

Windows Sanitizers GPU Conformance (AMD, self-hosted) Cross-Vendor Conformance Fuzz Smoke SonarCloud Scan Pre-commit checks

OloEngine is a cross-platform (Windows / Linux) C++23 interactive-application and rendering engine. It started as a follow-along of Hazel but has since grown well past that scope into a full ECS-driven engine with a physically-based OpenGL 4.6 renderer, 2D/3D physics, dual C#/Lua scripting, networking, navigation, procedural terrain and environmental systems, a gameplay/AI stack, and an ImGui editor.

Screenshots

Metallic and dielectric PBR spheres (a roughness sweep) reflecting a procedural star-nest sky:

PBR materials reflecting a procedural sky

PBR modelAnimated models3D physicsFFT ocean
PBR modelAnimated models3D physicsFFT ocean

Table of contents

Repository layout

Target / directoryWhat it is
OloEngine/The engine static library (OloEngine). All subsystems live under src/OloEngine/<Subsystem>/; platform glue under src/Platform/.
OloEditor/ImGui-based editor (OloEditor). Panels under src/, runtime assets under assets/, a sample game under SandboxProject/.
OloRuntime/Standalone game runtime (OloRuntime) that loads what the editor builds.
OloServer/Headless dedicated server (OloServer) — the one target that also runs on WSL2.
OloEngine-ScriptCore/C# scripting API library, built with dotnet build under any generator; C# scripting runs on Windows only.
OloEngine-LuaScriptCore/Lua scripting core (all platforms).
OloEngine/tests/GoogleTest suite (OloEngine-Tests).
tools/Build-time tooling, notably OloHeaderTool (code generation — see Tooling).
docs/Design docs, ADRs, build/testing guides, and agent rules — see Documentation.

Getting started

Supported platforms

  • Windows — Visual Studio 2026 (the default msvc preset) or Visual Studio 2022.
  • Linux — GCC 14+.
  • WSL2 — can compile every target and run OloServer, but not OloEditor: WSL2's software renderer only exposes OpenGL 4.5, and the editor requires a native OpenGL 4.6 GPU.

Requirements

  • CMake 4.2+ — required by CMakePresets.json (the msvc preset's Visual Studio 18 2026 generator only exists in 4.2+). The root CMakeLists.txt itself only requires 3.27, so a plain cmake -B build -G "Visual Studio 17 2022" still works without presets.
  • Python 3.10+ with the jinja2 package (needed to build glad2).
  • Vulkan SDK — for SPIR-V shader compilation.
  • Steamworks SDK — optional, and you must download it yourself: its licence forbids redistribution, so it is neither in this repo nor available through vcpkg. Needed only to work on Steam features (achievements, Steam Cloud, rich presence/overlay). Steam support is auto-detected: OLO_WITH_STEAM turns on when STEAMWORKS_SDK_ROOT points at a real SDK (a public/steam directory) and stays off otherwise — so without the SDK everything builds, runs and tests exactly as before, and with it installed you get the Steam path without passing a flag. The SDK itself is free — an ordinary Steam account and the Steamworks SDK Access Agreement, not the $100 Steam Direct publishing fee. Setup: docs/ops/build.md.

Clone

git clone https://github.com/drsnuggles8/OloEngineBase

Most third-party dependencies are resolved from the vcpkg.json manifest and built once into vcpkg's machine-global binary cache, so a second worktree restores them in seconds instead of recompiling (issue #773). VCPKG_ROOT must point at a bootstrapped vcpkg clone — see docs/ops/build.md; a configure without it fails with a pointer there. A handful of dependencies are still fetched at configure time via FetchContent/CPM into OloEngine/vendor/ (never edit that directory — a CMake reconfigure wipes it). CMake also creates the build/ directory with the generated solution files.

Building & running

CMake presets (CMakePresets.json):

PresetGenerator / build dirPurpose
msvcVisual Studio 18 2026 → build/Primary, default build dir
clangclNinja Multi-Config → build-clang/clang-cl warnings with the MSVC ABI
clangcl-asanNinja Multi-Config → build-clang/Adds AddressSanitizer
# Generate the Visual Studio solution
scripts\Win-GenerateProjectVS2026.bat        # or Win-GenerateProjectVS2022.bat

# Build a target
cmake --build build --target OloEditor       --config Debug --parallel
cmake --build build --target OloEngine-Tests --config Debug --parallel
cmake --build build --target OloRuntime      --config Debug --parallel
cmake --build build --target OloServer       --config Debug --parallel

# ClangCL (configure once, then build)
cmake --preset clangcl
cmake --build build-clang --target OloEngine-Tests --config Debug --parallel

Working directory matters. OloEditor, OloRuntime, and OloServer resolve assets, shaders, and Mono assemblies relative to OloEditor/. Always run them with cwd = OloEditor/. The VS Code tasks already do this.

VS Code users — predefined tasks in .vscode/tasks.json wrap all of the above: build-oloeditor-debug, run-oloeditor-debug, build-tests-debug, run-tests-debug, configure-clangcl, etc. Run them from the Command Palette (Ctrl+Shift+P → Tasks: Run Task).

CLion users — open the repo folder, let CLion configure the CMake project, then set the working directory of the OloEditor run configuration to the OloEditor/ folder.

The full per-OS build matrix (including Linux and WSL specifics) lives in docs/ops/build.md.

Features

Core engine

  • Entity-Component-System built on EnTT under a UUID-keyed Entity wrapper. The hottest update loops use EnTT owning groups for packed iteration.
  • Deterministic gameplay scheduler — per-tick systems declare their read/write data flow and the execution order is derived by topological sort, with optional worker-thread parallelism (audited per system) and a physics-shadow phase that steps the physics world alongside game-thread systems.
  • Multi-threaded asset system — async loading with hot-reload via filewatch; typed retrieval through AssetManager.
  • Dual scripting — C# via Mono (Windows) and Lua via Sol2 (all platforms), with generated bindings.
  • YAML serialization — scenes, entities, and prefabs. Trivial component (de)serialization, the ECS component tuple, save-game capture/restore lists, and add/remove handlers are all code-generated from the component definitions (see Tooling), so most new components round-trip with zero hand-written glue.
  • Save games — component-level snapshot/restore with a versioned binary archive format.
  • Custom memory management, threading, and task systems — custom allocators, RAII resources, a job/task scheduler, and async primitives.

Rendering

  • Modern OpenGL 4.6 with Direct State Access and SPIR-V shader compilation.
  • Stateless, layered, multi-threaded command queue (Molecular Matters' design) driven by a render-graph.
  • Physically-based rendering — metallic-roughness workflow, image-based lighting (IBL).
  • Deferred + forward paths with Hi-Z GPU occlusion culling and LOD.
  • Post-processing & advanced lighting — SSAO, SSR, SSGI, contact shadows, all shadow types (incl. PCSS), TAA, volumetric fog + god rays, screen-space decals, order-independent transparency, and FSR/CAS upscaling.
  • Unified 2D/3D pipeline — sprites and 3D meshes through one renderer.
  • Skeletal animation — Assimp-imported skeletons, animation graphs and state machines, additive layers, and humanoid retargeting; secondary motion via spring bones and cloth.
  • GPU-driven particles — compute-based particle simulation.
  • Real-time particle fluids — GPU Position-Based Fluids solver (compute spatial hash + density-constraint solve) with screen-space fluid rendering, deterministic CPU fallback, and two-way Jolt rigid-body coupling.

Physics

  • 3D — Jolt Physics for rigid bodies, joints, characters, and vehicles.
  • 2D — Box2D for 2D mechanics.
  • Custom collision layers for flexible filtering and detection.

Environmental & world systems

  • Procedural terrain generation with GPU-driven quadtree LOD, and optional virtual texturing — a feedback-driven page cache that moves the splat blend off the per-pixel path.
  • Water / FFT ocean rendering.
  • Weather & environment — wind, snow (with deformation), and precipitation systems.
  • Streaming volumes, light-probe volumes, fog volumes, and navigation-mesh bounds as scene-level authored data.

Gameplay, AI & narrative

  • AI — GOAP planner / action system and a perception system (sight / sound / awareness).
  • Navigation — Recast/Detour nav-mesh generation with nav agents.
  • Dialogue & quests, plus a localization system driving LocalizedTextComponent.
  • Cinematic sequencer — timeline-driven cutscenes.

Networking

  • Reliable UDP transport over GameNetworkingSockets, with encrypted transport (libsodium) and a protobuf wire format for snapshots / RPC. OloServer provides the headless dedicated-server target.

Audio & video

  • 3D spatial audio (miniaudio) with source/listener ECS components and a SoundGraph / MetaSounds-style audio-graph.
  • Video playback component.

UI system

  • ECS-based UI — Canvas, RectTransform, Panel, Text, Image, Button, Slider, Checkbox, Toggle, Progress Bar, Input Field, Dropdown, Scroll View, Grid Layout, WorldAnchor, and more.
  • Anchor layout — RectTransform-style anchoring, pivots, and auto-layout via grid containers; works in both 2D and 3D.
  • Editor integration — a "Create UI" menu, per-component property panels, and editor-time preview.
  • Full C# and Lua bindings for every UI component.

Editor & debugging

  • ImGui editor — scene hierarchy, content/asset browser with drag-and-drop, gizmos and transform tools, and real-time scene manipulation.
  • Play-in-editor — snapshot/restore around Play/Simulate.
  • Profiling — Tracy integration plus custom renderer profiler and GPU/CPU memory tracking.
  • Read-only MCP diagnostics server — inspect a live editor frame (screenshots, camera control, intermediate render targets, shader errors) from an external tool. See docs/guides/mcp-diagnostics-server.md.

Testing

The suite (OloEngine-Tests, GoogleTest) spans two independent axes:

  1. A renderer testing pyramid (L1–L11 plus plumbing / culling-LOD / shader-pipe / integration layers) covering pixels, GL state, and shader math — including screenshot-based visual-regression tests that gate GPU-equipped runs and skip cleanly when no OpenGL 4.6 context is present.
  2. A Functional / cross-subsystem axis ("Functional" tag) driving real Scene::OnUpdateRuntime ticks across Animation × Physics × Scripting × Networking × Audio × Asset × Nav × Save-game × Gameplay × AI seams.
# Run everything, or a single test
build\OloEngine\tests\Debug\OloEngine-Tests.exe
build\OloEngine\tests\Debug\OloEngine-Tests.exe --gtest_filter=SuiteName.TestName

The rationale, per-layer reference, classification rules, and anti-patterns live in docs/testing.md and docs/agent-rules/testing-architecture.md.

Continuous integration

Per-PR and per-push

These gate every change and run on GitHub-hosted runners.

WorkflowStatusCovers
WindowsWindowsMSVC build + full suite via CTest
SanitizersSanitizersASan (Windows/clang-cl), ASan+LSan / UBSan / TSan (Linux/Clang)
SonarCloud ScanSonarCloud ScanStatic analysis (see sonarqube-review-alignment.md)
Pre-commit checksPre-commit checksFormatting, YAML, test-catalogue classification

Scheduled

Cron times are UTC — GitHub's scheduler does not observe DST, so local times shift by an hour across the year. Every one of these is also workflow_dispatch-able from the Actions tab. Scheduled runs may start tens of minutes late under load.

WorkflowStatusCadence (UTC)Runs onCovers
GPU Conformance (AMD)GPU Conformance (AMD, self-hosted)nightly 00:47self-hosted Linux / AMD Navi 10The renderer's visual, golden and perf layers against a real GPU — they skip everywhere else. See self-hosted-gpu-runner.md
Cross-Vendor ConformanceCross-Vendor Conformancenightly 03:47GitHub-hosted WindowsL1/3/5/6/8 against Mesa llvmpipe (software)
Fuzz SmokeFuzz Smokenightly 04:13GitHub-hosted WindowsParser / deserializer fuzzing
Flaky repro (#281)Flaky repro (#281)nightly 07:00GitHub-hosted WindowsRepeat-runs a known-flaky test to hunt issue #281
Video (FFmpeg backend)Video (FFmpeg backend)weekly, Mon 06:00GitHub-hosted matrixOLO_VIDEO_FFMPEG=ON build + decode tests
reflection-status-checkreflection-status-checkmonthly, 1st 09:07GitHub-hosted LinuxReminder to re-check C++26 reflection toolchain support

Hardware-GPU vendor coverage is AMD-only. The goldens in assets/tests/golden/ are baselined on NVIDIA, but only ever from a developer machine — there is no NVIDIA or Intel CI. llvmpipe is a third software implementation, not a second vendor, so it catches spec-compliance drift rather than driver divergence.

Tooling

OloHeaderTool (tools/OloHeaderTool/) scans OloEngine/src/ and generates, at build time (via the GenerateBindings target):

  • C++ and C# scripting bindings from OLO_PROPERTY annotations.
  • The ECS AllComponents type list, the save-game capture/restore lists, the OnComponentAdded/OnComponentRemoved no-op handlers, and per-component scene serialize/deserialize blocks — all derived from the struct *Component definitions.

This collapses what used to be several hand-maintained, easy-to-forget touch-points into codegen: an all-trivial component now round-trips through scenes, save-games, and scripting with little or no hand-written glue. See CLAUDE.md for the exact contract when adding or changing a component.

Dependencies

Most dependencies come from the vcpkg.json manifest, pinned as a whole by its builtin-baseline registry commit and built into vcpkg's binary cache. The ones marked (in-tree) below are still fetched via FetchContent/CPM into OloEngine/vendor/, each for a specific reason documented in that file's header comment — briefly: sol2 and Lua are pinned to a tested pair, stb's port is ~2 years behind on an image decoder, ImGuizmo's port would drag in a second Dear ImGui, and the single-header / forked / generator-driven ones have no port at all. OpenUSD and FFmpeg have their own build paths (see CLAUDE.md).

Core — entt (ECS) · glm (math) · spdlog (logging) · yaml-cpp (serialization) · nlohmann/json (tools / IPC) · choc (utilities, in-tree) · atomic_queue (lock-free MPMC queue) · meshoptimizer

Rendering — glad (in-tree) · glfw · assimp · stb (in-tree) · zlib

Physics — Jolt Physics (3D) · box2d (2D)

Networking — GameNetworkingSockets · libsodium · protobuf

Navigation — recastnavigation

Audio — miniaudio

Scripting — sol2 (in-tree) · lua (in-tree) · Mono (C# runtime, manually integrated)

UI & editor — imgui (in-tree) · imguizmo (in-tree)

Development & profiling — tracy · googletest · filewatch (in-tree)

Code style & pre-commit hooks

We enforce a small set of formatting rules via pre-commit:

python -m pip install --user pre-commit
pre-commit install                 # enable the git hook (run in the repo root)
pre-commit run --all-files         # optional: apply across the whole repo

Included hooks: trailing-whitespace, end-of-file-fixer, and clang-format for C/C++ (uses the repo-root .clang-format). An .editorconfig mirrors the whitespace rules for editors. The hooks ignore vendor/, build/, and IDE metadata folders. A GitHub Action runs pre-commit on every push and PR.

Documentation

Everything under docs/ — start at docs/README.md for the full index. Highlights:

  • docs/ops/build.md — full Windows / Linux / WSL build matrix.
  • docs/testing.md — the canonical testing opinion doc.
  • docs/guides/ — subsystem how-tos (AI/GOAP, perception, terrain, UI, localization, cinematics, video, MCP diagnostics).
  • docs/adr/ — architecture decision records.
  • CLAUDE.md — contributor/agent playbook: build, test, and the component cross-binding contract.

Influences & references

  • Hazel — the foundation this engine started from (The Cherno's game-engine series).
  • Lumos — ideas for the OpenGL renderer and engine architecture.
  • Arc — ideas for the audio engine.
  • Molecular Matters Blog — the multi-threaded render command queue architecture.
Source 2 files
hooks/register.ts 41 lines
1import type { EngineInterface, Register } from 'claude-code'
2
3import { formatTraps, matchTraps, type Trap } from './traps'
4
5const REPEAT_WINDOW_MS = 20 * 60 * 1000
6
7// Each trap is pointed out once per window: once is the help, every time is noise.
8const lastShown = new Map<string, number>()
9
10export const register: Register = on => {
11  on('tool.call', async ($, e, next) => {
12    const ran = await next(e)
13    if (ran.deny !== undefined || typeof ran.text !== 'string' || ran.text.length === 0) {
14      return ran
15    }
16    const context = {
17      tool: e.tool,
18      command: e.tool === 'Bash' || e.tool === 'PowerShell' ? e.command : '',
19      text: ran.text,
20      isError: ran.isError === true,
21    }
22    const traps = await unseen($, matchTraps(context))
23    return traps.length === 0 ? ran : { ...ran, context: [...(ran.context ?? []), formatTraps(context, traps)] }
24  })
25}
26
27async function unseen($: EngineInterface, traps: Trap[]): Promise<Trap[]> {
28  if (traps.length === 0) {
29    return traps
30  }
31  const now = await $.clock.now()
32  return traps.filter(t => {
33    const last = lastShown.get(t.id)
34    if (last !== undefined && now - last < REPEAT_WINDOW_MS) {
35      return false
36    }
37    lastShown.set(t.id, now)
38    return true
39  })
40}
41
hooks/traps.ts 176 lines
1// Error signatures of traps this repo has already paid for, each with the fix its memory
2// note records. Adding one: a new entry here plus a test line in tests/traps.test.ts.
3//
4// Keep a signature specific: the advice is attached the moment the text appears, and a
5// note that fires on ordinary output trains everyone to ignore it.
6
7export type TrapContext = {
8  tool: string
9  /** The shell command, for Bash / PowerShell; empty otherwise. */
10  command: string
11  /** What the tool printed (or its error). */
12  text: string
13  isError: boolean
14}
15
16export type Trap = {
17  id: string
18  title: string
19  /** The memory note that records it, by file name. */
20  memory: string
21  matches: (c: TrapContext) => boolean
22  advice: (c: TrapContext) => string
23}
24
25const isShell = (c: TrapContext) => c.tool === 'Bash' || c.tool === 'PowerShell'
26const heredocPython = (c: TrapContext) => /<<-?\s*['"]?\w+['"]?/.test(c.command) && /\bpython\d?(?:\.exe)?\b/.test(c.command)
27
28export const TRAPS: readonly Trap[] = [
29  {
30    id: 'heredoc-backslash',
31    title: 'backslashes lost in a heredoc Python edit',
32    memory: 'bash-heredoc-python-backslashes-get-unescaped.md',
33    matches: c =>
34      isShell(c) &&
35      heredocPython(c) &&
36      /SyntaxWarning: invalid escape sequence|Unterminated string literal|unterminated string|missing terminating ['"] character|error C2001: newline in string literal/.test(c.text),
37    advice: () =>
38      'The Bash tool collapses backslash escapes in a heredoc before Python sees them (`\\n` and `\\\\n` both become a real newline). ' +
39      'Look at the line that was actually written (`grep -n`/`cat -A`) before trusting the edit; build backslashes with `chr(92)`, or use the Edit tool.',
40  },
41  {
42    id: 'newline-in-string-literal',
43    title: 'a raw newline inside a string literal',
44    memory: 'raw-newline-in-string-literal-is-clang-warning-msvc-error.md',
45    matches: c => /error C2001: newline in string literal|warning: missing terminating ['"] character/.test(c.text),
46    advice: () =>
47      'A real line break landed inside a "..." literal: clang-cl only warns, MSVC (Windows CI) fails about an hour in. ' +
48      'It is usually a heredoc edit writing `\\n` as a newline; fix the literal and scan the changed files for split literals.',
49  },
50  {
51    id: 'edit-eperm-during-build',
52    title: 'Edit/Write refused while a build reads the file',
53    memory: 'edit-tool-blocked-while-build-reads-header.md',
54    matches: c => (c.tool === 'Edit' || c.tool === 'Write') && /EPERM|atomic write failed|ftruncate/i.test(c.text),
55    advice: () =>
56      'A running build holds the header open, so the atomic rename-over is refused; the file is untouched (check `git diff --stat`). ' +
57      'Wait for the build, or write in place from Python: `open(p, "r+b")`, `seek(0)`, `write`, `truncate()` (keep CRLF if the file has it).',
58  },
59  {
60    id: 'mcp-port-excluded',
61    title: 'the editor could not bind its MCP port',
62    memory: 'mcp-port-in-hyperv-excluded-range.md',
63    matches: c => /\[MCP\] Failed to bind 127\.0\.0\.1:\d+/.test(c.text),
64    advice: c => {
65      const port = /\[MCP\] Failed to bind 127\.0\.0\.1:(\d+)/.exec(c.text)?.[1] ?? '?'
66      return (
67        `Port ${port} is probably in a Hyper-V excluded range (34045–34944, 42271–42370, 49726–50464, …; \`netsh int ipv4 show excludedportrange protocol=tcp\`) ` +
68        "or another session's editor holds it. Stop only YOUR editor and relaunch with `-McpPort` outside every range (7391 or 7397 worked). Never delete another port's discovery file."
69      )
70    },
71  },
72  {
73    id: 'unmerged-index',
74    title: 'conflicted paths in the index',
75    memory: 'merge-state-can-be-lost-leaving-a-bare-uu-entry.md',
76    matches: c => isShell(c) && /\bgit\b[^\n]*\bstatus\b/.test(c.command) && /^UU /m.test(c.text) && !/still merging|unmerged paths/i.test(c.text),
77    advice: () =>
78      'First find which operation left it: `git rev-parse -q --verify` MERGE_HEAD, CHERRY_PICK_HEAD and REVERT_HEAD, and `rebase-merge/` or `rebase-apply/` under ' +
79      '`git rev-parse --absolute-git-dir`. A cherry-pick, revert or rebase in progress is not this trap: finish it with its own `--continue`. ' +
80      'Only if you were MERGING and none of those exists has the merge marker been lost while stages 1/2/3 are intact, and committing would record no merge parent. ' +
81      'Restore MERGE_HEAD only with the EXACT commit that merge named (the sha `git merge <ref>` resolved at the time, e.g. from `git reflog show <ref>` or FETCH_HEAD for a pull), not the ref\'s current tip: ' +
82      'a stage-3 blob identifies a file version, not a commit, so a matching blob is only a consistency check. ' +
83      'If the exact commit cannot be recovered, do not restore the marker: copy the resolved files aside, `git merge --abort`, redo the merge, and re-apply the resolution.',
84  },
85  {
86    id: 'worktree-add-powershell',
87    title: '`git worktree add` failing from the PowerShell tool',
88    memory: 'git-worktree-add-fails-from-powershell-tool.md',
89    matches: c => c.tool === 'PowerShell' && /worktree\s+add/.test(c.command) && /fatal: invalid reference/.test(c.text),
90    advice: () => 'The ref is fine: the PowerShell tool mangles the argument list. Run the same `git worktree add` from the Bash tool, and do not create the memory junction for a worktree that failed.',
91  },
92  {
93    id: 'gh-pr-checks-json',
94    title: '`gh pr checks` has no --json here',
95    memory: 'gh-pr-checks-has-no-json-flag.md',
96    matches: c => isShell(c) && /gh\s+pr\s+checks/.test(c.command) && /unknown flag: --json/.test(c.text),
97    advice: () =>
98      'It exits 0 with this error, so a poll loop on it waits forever. Use `gh pr view <n> --json statusCheckRollup --jq ...` or the check-runs API (`gh api repos/<o>/<r>/commits/<sha>/check-runs`).',
99  },
100  {
101    id: 'no-jq',
102    title: 'jq is not installed here',
103    memory: 'no-jq-on-this-box.md',
104    matches: c => /jq: command not found|'jq' is not recognized|jq: not found/.test(c.text),
105    advice: () => "Use gh's own `--jq` flag or a `python -c` one-liner. A watch script that pipes into jq reports nothing, silently, inside a Monitor.",
106  },
107  {
108    id: 'gh-projects-classic',
109    title: 'the Projects (classic) GraphQL error',
110    memory: 'gh-cli-gotchas.md',
111    matches: c => /Projects \(classic\) is being deprecated/.test(c.text),
112    advice: () =>
113      'Read with explicit fields (`gh issue view <n> --json title,body ...`), which skips projectCards. For writes, `gh pr edit --add-label` and `--body-file` exit 0 WITHOUT applying: use the REST API.',
114  },
115  {
116    id: 'gh-unknown-json-field',
117    title: 'a JSON field this gh build does not have',
118    memory: 'gh-cli-gotchas.md',
119    matches: c => /Unknown JSON field: "(?:closingIssuesReferences|stateReason)"/.test(c.text),
120    advice: () => 'This gh build lacks the field. Closing references: `gh api graphql` (pullRequest.closingIssuesReferences). Close reason: the REST issue object (`gh api repos/<o>/<r>/issues/<n>`, `state_reason`).',
121  },
122  {
123    id: 'worktree-dir-busy',
124    title: 'an emptied worktree directory still held',
125    memory: 'worktree-remove-blocked-by-fsmonitor.md',
126    matches: c => isShell(c) && /worktree\s+remove|rmdir|Remove-Item/.test(c.command) && /Permission denied|Device or resource busy/.test(c.text),
127    advice: () =>
128      'It is not the fsmonitor daemon. Run `.claude/scripts/free-locked-dir.ps1 -Path <dir>` (report first, then -Kill); if it finds no cwd holder, search `Win32_Process` CommandLines for the slug: leftover loops of the removed worktree\'s own session hold it.',
129  },
130  {
131    id: 'editor-holds-link-output',
132    title: 'a running binary blocks the link',
133    memory: 'build-file-locks.md',
134    matches: c => /lld-link: error: failed to write output '[^']*(?:OloEditor|OloEngine-Tests|OloRuntime|OloServer)\.exe': permission denied|LNK1104: cannot open file '[^']*\.exe'/i.test(c.text),
135    advice: () =>
136      "Every TU compiled; the link failed because this worktree's editor or test binary is still running (check the exe path before killing anything: sibling worktrees run the same names). " +
137      'ninja stopped at that point, so other targets of the same build did not link either: rebuild after stopping it.',
138  },
139  {
140    id: 'llvmgold-noise',
141    title: 'the LLVMgold warning in a Linux configure',
142    memory: 'llvmgold-warning-is-noise-read-past-it.md',
143    matches: c => /LLVMgold\.so: cannot open shared object file/.test(c.text),
144    advice: () => "Not the failure: it is check_ipo_supported's own probe, and configure continues (`IPO/LTO supported by this toolchain: NO`). Read past it for the real error.",
145  },
146  {
147    id: 'far-near-macro',
148    title: '`far` / `near` are macros under <windows.h>',
149    memory: 'win32-far-near-macros-break-locals.md',
150    matches: c => /expected unqualified-id/.test(c.text) && /\b(?:f32|f64|float|double|auto|const|int|u32)\s+(?:far|near)\b/.test(c.text),
151    advice: () => '<windows.h> defines `far` and `near` to nothing; a local of that name breaks, and the second error lands at its next use. Rename it (`farField`, `nearClip`).',
152  },
153  {
154    id: 'tmp-path-mismatch',
155    title: "Git Bash's /tmp is not Python's /tmp",
156    memory: 'git-bash-tmp-is-not-python-tmp.md',
157    matches: c => isShell(c) && /No such file or directory: '\/tmp\/|FileNotFoundError: [^\n]*'\/tmp\//.test(c.text),
158    advice: () => "In a Bash command /tmp is Git Bash's mount (%TEMP%); inside Python it is C:\\tmp. Use the session scratchpad, or `cygpath -w /tmp/x` to hand Python the real path. `set -e` did not stop after a failing heredoc either.",
159  },
160]
161
162export function matchTraps(c: TrapContext): Trap[] {
163  return TRAPS.filter(t => {
164    try {
165      return t.matches(c)
166    } catch {
167      return false
168    }
169  })
170}
171
172export function formatTraps(c: TrapContext, traps: readonly Trap[]): string {
173  const lines = traps.map(t => `- ${t.title} (memory: ${t.memory}): ${t.advice(c)}`)
174  return `olo-known-traps: this output matches a trap already recorded here:\n${lines.join('\n')}`
175}
176