Skip to content

Latest commit

 

History

History
198 lines (165 loc) · 12.7 KB

CLAUDE.md

File metadata and controls

198 lines (165 loc) · 12.7 KB

Project Instructions for AI Agents

This file provides instructions and context for AI coding agents working on this project.

Beads Issue Tracker

This project uses bd (beads) for issue tracking. Run bd prime to see full workflow context and commands.

Quick Reference

bd ready              # Find available work
bd show <id>          # View issue details
bd update <id> --claim  # Claim work
bd close <id>         # Complete work

Rules

  • Use bd for ALL task tracking — do NOT use TodoWrite, TaskCreate, or markdown TODO lists
  • Run bd prime for detailed command reference and session close protocol
  • Use bd remember for persistent knowledge — do NOT use MEMORY.md files

Architecture in one line: issues live in a local Dolt DB; sync uses refs/dolt/data on your git remote; .beads/issues.jsonl is a passive export. See https://github.com/gastownhall/beads/blob/main/docs/core-concepts/sync-concepts.md for details and anti-patterns.

Agent Context Profiles

The managed Beads block is task-tracking guidance, not permission to override repository, user, or orchestrator instructions.

  • Conservative (default): Use bd for task tracking. Do not run git commits, git pushes, or Dolt remote sync unless explicitly asked. At handoff, report changed files, validation, and suggested next commands.
  • Minimal: Keep tool instruction files as pointers to bd prime; use the same conservative git policy unless active instructions say otherwise.
  • Team-maintainer: Only when the repository explicitly opts in, agents may close beads, run quality gates, commit, and push as part of session close. A current "do not commit" or "do not push" instruction still wins.

Session Completion

This protocol applies when ending a Beads implementation workflow. It is subordinate to explicit user, repository, and orchestrator instructions.

  1. File issues for remaining work - Create beads for anything that needs follow-up
  2. Run quality gates (if code changed) - Tests, linters, builds
  3. Update issue status - Close finished work, update in-progress items
  4. Handle git/sync by active profile:
    # Conservative/minimal/default: report status and proposed commands; wait for approval.
    git status
    
    # Team-maintainer opt-in only, unless current instructions forbid it:
    git pull --rebase
    bd dolt push
    git push
    git status
  5. Hand off - Summarize changes, validation, issue status, and any blocked sync/commit/push step

Critical rules:

  • Explicit user or orchestrator instructions override this Beads block.
  • Do not commit or push without clear authority from the active profile or the current user request.
  • If a required sync or push is blocked, stop and report the exact command and error.

Build & Test

The plugin does not build where it lives. /w/Unreal/VaCuus is the git + docs + beads tree; the editor loads and builds a separate clone at /w/Unreal/VcHost/Plugins/VaCuus. Edit code there, not here. (Why a clone and not a symlink: UBA aborts on symlinked plugins — cross-process rename-while-open. AdditionalPluginDirectories would remove the duplication entirely; the recipe and its trap are on VaCuus-akj.6.22.)

# Build (editor target)
cd /w/Unreal/UnrealEngine && ./Engine/Build/BatchFiles/Linux/Build.sh \
  VcHostEditor Linux Development -project=/w/Unreal/VcHost/VcHost.uproject

# Build (monolithic game target — exercises a different code path, check it before a milestone lands)
./Engine/Build/BatchFiles/Linux/Build.sh VcHost Linux Development -project=/w/Unreal/VcHost/VcHost.uproject

# Automation suite
/w/Unreal/UnrealEngine/Engine/Binaries/Linux/UnrealEditor-Cmd /w/Unreal/VcHost/VcHost.uproject \
  -ExecCmds="Automation RunTests VaCuus; Quit" -unattended -nullrhi -nosplash

# Headless visual run
/w/Unreal/UnrealEngine/Engine/Binaries/Linux/UnrealEditor /w/Unreal/VcHost/VcHost.uproject \
  -game -RenderOffscreen -ForceRes -resx=1920 -resy=1080 \
  -ExecCmds="vacuus.M2Demo, vacuus.M1HUD.AutoShot 10"

Dev-loop hazards. Every one of these cost real time at least once.

  • A detoured UBA build can hang forever: child clang++s spin at 100% CPU and UBT idles. Seen 2026-10-16: five children, ~30 MB RSS and three fds each (they never opened their .rsp), for 22 minutes; the UBT log stopped at All processes queued. UBA's own source has a path that gives exactly that picture: a child that reads its shared event slot as uninitialised gets a no-op SharedEvent::Set(), so the host never hears it, and an IsSet() that returns false at once (UbaEvent.cpp:292-312), so the detour's wait loop (UbaDetoursSharedPosix.cpp:1059-1073) spins for as long as the host lives. Three detoured builds since (2 to 223 actions) did not reproduce it. This workstation now builds undetoured: <bAllowUBAExecutor>false</bAllowUBAExecutor> in the user BuildConfiguration.xml keeps the UBA executor and drops the detours (ExecutorFactory.cs:47-51), the same as -NoUBA — log lines end in [NoUba], and a 2-action build went from 24 s to 12 s. Anywhere detours are on, that signature means: kill by PID and rerun with -NoUBA.
  • No editor may be running while you build — it holds the .so. Check pgrep -a UnrealEditor and kill by PID. Never pkill -f <pattern>: the pattern matches your own bash wrapper and kills your shell. For the same reason a wait loop until ! pgrep -f '<pattern>' never ends: it finds its own wrapper (2026-10-16, a 16 s build waited on for ten minutes).
  • -ExecCmds splits on COMMAS, not semicolons. A recipe of console commands written with ; runs as one malformed command and silently does nothing — no "not recognized" line anywhere. (Automation is the one exception — next bullet.) The value also swallows every argument after it (bShouldStopOnSeparator=false, ParseExecCommands.cpp:63) and the editor launcher re-appends -game at the END of the command line — so a single command with no trailing comma becomes vacuus.Foo -game and dies silently too. End the value with a comma: -ExecCmds="vacuus.Foo,".
  • Quit an automation run through Automation, not beside it: -ExecCmds="Automation RunTests X; Quit". Automation splits its OWN argument on ; (AutomationCommandline.cpp:582) and its own Quit (:726) exits once the run completes, with the exit code set from the results (:490-504). The comma form …, Quit makes Quit a separate console command dispatched at frame 0, which is deferred and never fires — the editor then idles until killed. Measured 2026-10-06 (bead VaCuus-w87.7): ; Quit exited by itself in 23 s with code 0; , Quit finished the test in 5 s and hung until a 150 s timeout. Kill by PID only if a run is genuinely stuck.
  • Read test counts from Saved/Logs/VcHost.log, not stdout — an interleaved UnrealTraceServer fork clobbers the tail of every run.
  • -resx/-resy are ignored offscreen without -ForceRes (you get 888×500).
  • Launch editors detached (setsid nohup … & disown) with an absolute .uproject path; a relative path gives "Project file not found" then SIGSEGV.
  • ShutdownModule never runs in an Automation RunTests …; Quit run — module-teardown fixes need a separate normal-quit run to verify.
  • vacuus.M1HUD.AutoShot N fires after max(N, 3) recorded frames, not published ones and not N — the counter is a floor. To photograph a transient state, make the state last longer.
  • BuildPlugin's HostProject editor leg rewrites Engine/Binaries/**/UnrealEditor.modules and can resurrect stale platform modules; the next editor launch dies on !bIsRunningPlatform (TargetPlatformManagerModule.cpp:1070). Fix: delete the stale .sos and the manifest.
  • exit 127 with no callstack = a STALE ENGINE MODULE, and the message is on stderr, not in the log. SOLVED 2026-08-03 (bead 68h), after two earlier investigations blamed glibc and static TLS. libUnrealEditor-XMPP.so was four months old, not part of the current target — a rebuild after deleting it took 2 s and did not recreate it — yet still listed in Engine/Binaries/Linux/UnrealEditor.modules. It imports FLLMScope's ctor and dtor, which today's libUnrealEditor-Core.so no longer exports (it exports FLLMScopeDynamic::* instead). Lazy binding defers the failure to the first CALL, so nothing fails at load and the whole process looks healthy until something touches that module. Enabling a stat GROUP is what pulls the trigger — FXmppModule::Tick runs SCOPE_CYCLE_COUNTER, and a master-enable is what first makes those scopes send messages — which is why this looked for two rounds like a stats or a TLS bug. stat fps never master-enables collection, so it never fires; a packaged monolithic build has no dlopen at all. Four-run evidence chain, each "survive 60 s": stale .so + stat slate → dead 127; moved aside → lives, 743 frames; restored → dead 127 again; removed + vacuus.RefHud, stat vacuus (the owner's original report) → lives. Diagnosis, in one step: run in a terminal and read STDERR — the log ends mid-line but the process prints symbol lookup error: <path>.so: undefined symbol: <mangled>. Compare that .so's mtime with libUnrealEditor-Core.so; older means stale, delete it. This is the same family as the BuildPlugin manifest hazard below, and CrashReportClient not being built here is what hid the message from every earlier run. vacuus.M1HUD.PerfLog 1 is still the instrument the passport was measured with — it does not depend on the stats system at all — but group stats now work here.

Architecture Overview

HTML/CSS UI (vendored RmlUi 6.x) rendered fully off the game thread.

One process-wide UI thread owns every Rml::Context — forced by RmlUi, whose interfaces, initialised flag and whole CoreData are process-global statics. The per-instance unit is the view (UVaCuusView, one context each); UVaCuusSubsystem owns its game instance's views and pulses the shared thread once per tick.

Each UI frame: drain command queue → Context::Update() → publish an interactive-region snapshot → Context::Render(), during which a record-only Rml::RenderInterface produces a command buffer. The buffer crosses to the render thread, which creates RHI resources and replays it into a persistent per-view render target; a Slate custom element composites that RT every engine frame. Publication is withheld when the frame's content hash is unchanged and no resource traffic occurred — on a static HUD that is ~13,000 recorded frames to 1 published.

The game thread only enqueues input and reads the snapshot. The snapshot is what makes the design work: Slate demands a synchronous Handled/Unhandled answer about geometry that lives on another thread, and the snapshot is that answer, cached once per frame so every handler in a frame sees one stable picture.

Modules: VaCuusRml (vendored library), VaCuus (UI thread, VFS, subsystem, view, snapshot, input mapping, IME), VaCuusRender (recorder, replayer, Slate element, UMG widget), VaCuusEditor (live reload).

Conventions & Patterns

  • Comments explain why, and cite engine or RmlUi source as file:line. A comment that restates the code is noise. Open every line you cite — seven comments in M2 asserted mechanisms or contracts that did not hold, and two of that milestone's four critical bugs were found by noticing a justification that contradicted the source it named.
  • Restore-the-bug is the proof standard. For any correctness claim: break it deliberately, watch the specific test fail, restore, and report both outcomes. A test that has never been seen to fail is not yet evidence.
  • An invariant with no observable cannot be tested and will rot. M2's critical cache bug survived implementation and its first review because RmlUi exposes no way to see its caches; the fix had to add a counter before the property could be asserted at all.
  • Prefer enforcement by shape over enforcement by comment. Make the wrong call fail to compile: a private member reachable only through the correctly-paired static; a member-count static_assert next to the array it guards. Note sizeof catches growth and offsetof catches reordering, but only a member count catches a field inserted into existing padding.
  • Never weaken the thread invariant. Every RmlUi call happens on the UI thread; other threads enqueue. Every cross-thread entry point asserts its thread.
  • Push back with sources. Being handed a wrong premise and implementing it anyway is the more expensive failure. This has happened repeatedly and been right every time.