Addresses Critical #1 + Important #2/#3/#4 from the Task 9 code review.
CRITICAL — Tab/Enter were stolen by CodeJar before the popup handler
saw them. CodeJar registers its keydown listener during construction
(line ~159), so it ran first in bubble order: Tab handler
preventDefaulted and inserted 2 spaces, Enter handler preventDefaulted
+ stopPropagation'd (with leading indent), so the popup-accept either
ran on corrupted state or never fired at all. Fix: register the popup
listener with {capture: true} and call stopPropagation on the keys we
own — that way capture phase fires before CodeJar's bubble listener
and the key is fully consumed by the popup while it's visible. Normal
typing (popup hidden) early-returns without stopPropagation, so
CodeJar's tab-indent + enter-preserve-indent still work when there's
no autocomplete to accept.
IMPORTANT — destroy() leaked the popup <ul> into document.body. Each
mount/destroy cycle (e.g. modal close/reopen) left an orphan popup.
Fix: pop.remove() in destroy().
IMPORTANT — async refreshPopup could race in stale renders if the
first keystroke fired the vocab fetch and the second keystroke
captured a different ctx before the fetch resolved. Fix: warm the
cache with a fire-and-forget loadVocab(language) at mount, so the
first user keystroke hits cache. Eliminates the only realistic window
for the race.
IMPORTANT — acceptCompletion's Range.setStart could throw
IndexSizeError on pathological state (caret inside a tokenized span
where the fragment isn't fully upstream). Fix: try/catch the entire
DOM mutation block, log + dismiss on failure. Plus an inline comment
documenting the single-text-node invariant the current grammars hold.
Plan source updated for the capture-phase fix (most important for
future regeneration); the other fixes are smaller and only mirrored
into the actual code.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Task 9 plan template used the captured \`jar\` closure variable in
acceptCompletion, which becomes stale after setLanguage's
tear-down-and-remount. Same class of bug Task 4's review caught and
fixed. Update the plan to match the correct implementation.
The plan template (and verbatim implementation) listed five of the six
editor asset URLs in the structural test — vendor/prism.css was
omitted. If a future change drops the Prism stylesheet from the
partial, syntax tokens lose their color rules silently and the test
still passes. Add the missing assertion and update the plan to match.
Addresses Minor #1 from the Task 6 code review.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses two Minor follow-ups from the Task 4 code review:
- findFilenameInput previously included `body` in its closest() selector,
meaning any "auto" textarea outside a modal would walk all the way up
and pick up the files-editor modal's filename input from elsewhere in
the document. Drop `body` so out-of-modal "auto" usage degrades
cleanly to "plain".
- setValue now dispatches an `input` event on the textarea after
writing, matching the onUpdate mirror. Task 10 wires the files-editor
modal to call setValue when loading file content — without this fix,
textarea-listening code (e.g. unsaved-changes indicators) wouldn't
see programmatic loads. Now setValue and user typing produce the
same observable side effects.
Plan source block updated to match.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The plan had Step 1 (initial widget) + Step 2 (setLanguage patch); the
implementation merges them into one final file. Update the plan to
show the final file verbatim so a future regeneration produces the
same output. Step 2 in the plan is renumbered to 'Manual verification
note' (just the deferred-to-Task-6 sentence) for completeness.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses Important #1 + Minor #2/#3 from the Task 3 re-review:
- --color-bg-popover-active light value: #f3f4f6 → #e5e7eb. The prior
value was within ~1.05:1 luminance of the white surface — keyboard
navigation through the autocomplete list had no visible focus
indicator in light mode. e5e7eb (Tailwind gray-200) clears that.
- Drop dead fallback hexes on the four guaranteed tokens
(--color-string/-keyword/-number/-bg-popover-active). They never
fired post-fix and only produced a dark-mode-only palette if
tokens.css somehow failed to load — i.e. they were misleading.
- Plan source block (Task 3 Step 2) replaced with the post-fix CSS
verbatim + a new Step 2b that documents the tokens.css additions
alongside the editor.css template, so a fresh regeneration
produces the same file.
Deferred: cross-cutting --font-mono token (Minor #4 — would touch 7+
sites outside Task 3's scope).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses Important #1 + #2 from the Task 3 code review:
- Adds --color-string, --color-keyword, --color-number,
--color-bg-popover-active to tokens.css in both the :root and dark
blocks. GitHub-style palette tuned for legibility on each theme's
surface.
- Updates .editor-code to use the same padding tokens, font-family
stack, font-size, and line-height as the existing textarea rule so
the contenteditable doesn't visibly jump when the widget mounts.
- Drops the caret-color override (browser default adapts to system
theme — no token needed).
Plan source block updated to match.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The `\b` word boundary anchor prevents the optional minus from ever
matching from positions where signed numbers naturally appear (` -1`,
`(-1`, `=-1` all word-boundary-from-non-word and the `-?` fires zero
chars). Negative numbers are tokenised via the operator class instead,
which is the consistent behaviour the grammar already exhibits.
Plan source block updated to match so a fresh regeneration produces the
same file.
Addresses Minor #1 from the Task 2 code review.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Spec reviewer caught that the literal 'Prism.languages.bash' string
doesn't appear in the minified prism.js (minifier renames Prism→e).
Switch the verification grep to match either the bash shebang token
or any languages.bash assignment; both survive minification.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task 1 implementation discovered that codejar v4.0.0 ships its
browser bundle at /dist/codejar.js, not the package root. The vendor
README already records the correct URL; this patch keeps the plan
itself accurate for future regeneration.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The original Task 1 instructed a human to click through prismjs.com's
configurator UI — a step a subagent can't perform. Replace with direct
curl from jsdelivr for both Prism components (core + clike + bash
concatenated) and CodeJar, plus a sed-based ESM-export strip and a
window-global shim. Updates the vendor README template accordingly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lays out the file-by-file migration from the current three time-display
styles to the unified timeago filter from the design spec. TDD ordering
with tests-first, per-task commits, line-numbered locators, and an
explicit verification pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Today the Server Log panel shows the last 200 lines of the unit's entire
journal — mixing the current run with leftovers from prior starts. Filter
on systemd's per-(re)start InvocationID so the panel begins at the most
recent start, idles with keepalives when the unit has never run, and
force-disconnects on restart so the SSE client reconnects to the new run.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Migrate from pip-install-e + setuptools to a uv workspace with a
committed uv.lock for deterministic deps. Switch both members to
hatchling, and move package sources into nested standard layout
(l4d2host/l4d2host/, l4d2web/l4d2web/) so builds work from a
read-only source tree — setuptools wrote egg-info to source under
the old layout, which broke uv sync on the root-owned /opt/left4me/src.
Local dev install: `pip install -e ./l4d2host -e ./l4d2web` -> `uv sync`.
.envrc switches from `layout python python3.13` to `use uv`. Python
pinned to 3.13 via .python-version.
l4d2web now declares its cross-dep on l4d2host explicitly via
[tool.uv.sources] (workspace = true). l4d2web/alembic.ini and
l4d2web/alembic/ stay at the project root (standard alembic layout).
Test fixes:
- tests/__init__.py added to both test dirs so pytest doesn't shadow
l4d2host as a namespace package via outer-dir walk.
- 3 CWD-relative paths in tests (l4d2web/static/css/{tokens,layout}.css
and js/sse.js) anchored to Path(__file__) so they survive layout
changes.
- Two test_install.py tests now monkeypatch HOME to tmp_path so they
stop silently mutating ~/.steam/sdk32 on every run.
628 tests pass under sandboxed `uv run pytest`.
Per docs/superpowers/plans/2026-05-15-uv-workspace-execution.md;
prereq for the ckn-bw bundle's uv-sync action (queued).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Implementation plan for 2026-05-15-deployment-responsibility-design.md.
Bite-sized steps per task; each task ends with both repos committed
and ovh.left4me idempotent. Tasks: (1) sysctl consolidation canary,
(2) hardening drop-ins, (3) sudoers symlink, (4) scripts relocation
+ symlinks, (5) cleanup + docs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The hardening refactor that just landed closes the same-uid attack
surface (FS view, ptrace, /proc visibility, signals) for the web +
gameserver units via systemd directives plus system-wide
kernel.yama.ptrace_scope=2. Keeping the script-sandbox on a separate
uid was the inconsistent half-step — defense-in-depth only, with
build-time-idmap complexity attached. One principle wins: harden
once, share the uid.
scripts/libexec/left4me-script-sandbox: drop the idmap block (uid
lookups, STAGING setup, cleanup_staging trap, mount --bind
--map-users), switch User=/Group= to left4me, point BindPaths at
\$OVERLAY_DIR directly. Header comment updated to reflect
hardening-not-uid as the same-uid defense. nsenter self-wrap kept —
it's about mount-namespace escape, not uid.
Tests + comments + companion docs updated. Build-time-idmap and
overlay-idmap plans marked SUPERSEDED; user-uid-split spec revised
to "1 user is correct"; one-line update notes on the hardening
specs and the build-overlay-unit-design.
Companion ckn-bw commit removes the l4d2-sandbox user + group and
tightens /var/lib/left4me from 0711 → 0755 (the traverse-only mode
was specifically for the sandbox uid).
Approved-but-not-executed plan to collapse the two-user model
(left4me + l4d2-sandbox) into one. The build-time-idmap that
translates sandbox writes back to left4me uid becomes a no-op when
source uid == target uid, so it's removed along with ~30 lines of
helper plumbing. Hardening already covers the same-uid attack
surface the sandbox uid was defending against, so collapsing makes
the architecture consistent with the web/server hardening-only
decision.
Plan: docs/superpowers/plans/2026-05-15-uid-collapse.md
Handoff: docs/superpowers/specs/2026-05-15-session-handoff.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
12 tasks across left4me + ckn-bw: emitter verification, three Python
constants in the systemd_units reactor, spread into both managed units,
sysctl drop-in, annotated reference units, four spec bug fixes, mark
uid-split spec superseded, cross-repo push, bw apply + verify on host,
apt-remove test tooling. Each task has bite-sized steps with exact
commands and expected output.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the implementation plan that landed in the preceding commit
(2026-05-15-deploy-dir-rethink.md) under docs/superpowers/plans/, and
marks the two related specs:
- 2026-05-15-deploy-dir-rethink-design.md (the source handoff) gets a
"Resolved by …" banner at the top with a one-paragraph summary of
the decisions taken. Body preserved for archaeology.
- 2026-05-15-janitorial-cleanup.md gets a status banner noting that
items 1, 3, 4, 5 are fully resolved by the deploy-dir-rethink plan
and item 2 is partially resolved with a third option the original
enumeration didn't list: only the truly-dead two static units
(cake.service, nft-mark.service) deleted, the reactor-emitted set
(server@, web, workshop-refresh.{service,timer}, slices) retained
as curated examples. Resolved items left in place but flagged.
Remaining live janitorial items: 6 (bubblewrap doc drift), 7
(conditional on build-overlay-unit refactor), 8 (operational idmap
bind cleanup), 9 (Optimized Settings overlay verification), 10 (SM
1.13 calendar reminder).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Architectural cleanup: the uid translation is a build-time concern
(the sandbox produces sandbox-uid files); having the gameserver path
unwind that producer-side decision on every mount means the mount
helper carries idmap lifecycle code it shouldn't need. Moving the
idmap into the script-sandbox bind makes files land left4me-owned on
disk, drops ~140 lines from left4me-overlay, and makes all overlay
content (workshop + script-built) consistent on-disk.
Verified on left4.me kernel 6.12.86 that the kernel idmap propagates
through plain re-bind, so systemd-run's BindPaths can wrap a
pre-created idmapped staging path.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Persist the implementation plan for adding idmapped bind mounts to
left4me-overlay so that overlay copy-up from l4d2-sandbox-owned lower
layers (script-built overlays) produces left4me-owned upperdir entries
the gameserver can write. Mechanism verified end-to-end on ovh.left4me
in a temp dir on 2026-05-14.
Plan for adding a per-server RCON console: HTMX append-swap input form,
fixed-height scrolling transcript replayed from CommandHistory on load,
multi-packet response handling, owner-only access, 30s timeout.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The two indexes ix_sps_server_open and ix_sps_server_recent were
byte-identical because SQLAlchemy's Index(name, *cols) form drops the
DESC ordering the spec intended. Rather than reach for text("left_at
DESC"), drop the second index entirely — SQLite scans the ASC index
backwards at no measurable cost. Spec and plan updated to match.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds /profile reachable via header username, with change-password form
as its first section. Industry-standard session semantics: other sessions
invalidated on password change, current session kept, via new
users.password_changed_at column + session marker.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Eight TDD tasks: sysctl extension, nftables marking (file + unit), CAKE
shaper (env + helper + unit), deploy-script wiring, README. Each task
adds one artifact with its assertion in test_deploy_artifacts.py and
ends in its own commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captures the symptom (Reset blew up on `umount target busy`), the
false starts (eager retry, lazy fallback, TimeoutStopSec bump — all
shipped briefly and reverted), the actual root cause (the helper's
own Python interpreter inheriting and pinning the unit's mount
namespace), and the fix (nsenter at the systemd Exec line).
The lessons section is the part future-me reads first: a retry loop
is a hint that something we own is the blocker; probe `/proc/*/ns/mnt`
before assuming kernel async; `+` Exec prefix doesn't escape the
unit's mount namespace.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two TDD tasks: helper+service_control verb rename, then poller code
+ wiring + tests. Operator-side smoke test in F.3.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two TDD tasks: deploy-script cpuset block + tests, README
"CPU isolation" subsection. Operator-side smoke test in F.3.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Six tasks (TDD, one commit each): unit directives, slice files,
sysctl conf, sandbox slice + OOMScoreAdjust, deploy-script wiring,
README escape-hatch section. Final verification step with full
deploy + host + web pytest sweep.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each linked overlay gets a checkbox on the blueprint detail page that opts
its server.cfg in as exec server_overlay_<id>. The web app builds the
spec with {path, alias} per overlay and prepends exec server_overlay_<id>
lines to the blueprint config in lowest-overlay-first order. The host
stages those copies in the overlayfs upper layer before mounting (avoids
copy-up writes against a sandbox-uid file). A live preview block above the
Config textarea shows what gets auto-executed.
Schema:
- alembic 0007: BlueprintOverlay.expose_server_cfg BOOLEAN
Spec contract:
- l4d2host OverlayRef(path, alias?). load_spec accepts both bare-string
and {path, alias} entries.
Side effects folded in (same file in l4d2_facade):
- start_server auto-initializes; the manual Initialize step is no longer
needed before Start.
- initialize_server no longer runs blueprint builders — builds happen on
overlay save, not on every server Start.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The vendored static/vendor/htmx.min.js turned out to be a 33-byte
placeholder, so the hx-get/hx-target/hx-trigger attributes on the
overlay file tree's folder buttons were inert: clicks rotated the
chevron (own JS) but never fetched. Switch the lazy-load to a
~30-line plain-JS handler in static/js/file-tree.js that fetches
button.dataset.filesUrl on first expand and dedupes via dataset.loaded.
Update the spec/plan to match. Route + partial contracts unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captures the design rationale for the new overlay-detail Files section
(verify build output, click-to-download for individual files via Flask
send_file, HTMX-driven lazy folder expansion) and the paired
implementation plan that produced it. Adds .superpowers/ to .gitignore
so brainstorm session artifacts never sneak into a future commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captures the v3 design: IPAddressDeny= alone (no IPAddressAllow=any
because the documented "more specific wins" semantics don't hold on
systemd 257 / kernel 6.12 — the allow trumps unconditionally), explicit
CIDRs (the -p parser rejects the localhost/link-local shorthand
keywords), and a static sandbox-only resolv.conf bind to keep DNS
reachable when private RFC1918 ranges are blocked.
Plan documents what was implemented (in 7e66936) and the lessons
surfaced during execution so the next person doesn't have to rediscover
them.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Spec captures the v2 architecture (systemd-run service mode with full
hardening directives, no bwrap), the two surfaces in scope (helper
rewrite + bubblewrap dep removal + left4me.db mode tightening), and the
gotchas surfaced by smoke-testing the prototype on ckn@10.0.4.128:
- ProtectSystem=strict makes /var/lib/left4me visible (not invisible);
must add TemporaryFileSystem=/var/lib to mask it.
- Script bind via BindReadOnlyPaths uses ${SCRIPT}:/script.sh syntax.
- No PrivatePID= directive in systemd; host PIDs visible via /proc.
Information disclosure only — kernel UID-mismatch blocks signals.
Plan breaks the migration into 4 tasks (helper rewrite, deploy-script
deps + DB mode, host smoke-test, drift sweep) with explicit rollback.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captures the architectural fix for the mount-propagation bug: replace
fuse-overlayfs (rootless mount inside the web service's namespace, never
visible to host or to gameserver units) with kernel-native overlayfs
mounted via a privileged sudo helper that nsenters into PID 1's mount
namespace. Companion plan numbers the migration as five tasks ending in
end-to-end verification on the test box.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds two managed system overlays (l4d2center-maps, cedapug-maps) that
fetch curated map archives from upstream sources and reconcile addons
symlinks for non-Steam maps. A daily systemd timer enqueues a coalesced
refresh_global_overlays worker job; downloads, extraction, and rebuilds
run in the existing job worker and surface in the job log UI.
Schema: GlobalOverlaySource / GlobalOverlayItem / GlobalOverlayItemFile
plus nullable Job.user_id so system jobs render as "system" in the UI.
The new builder reconciles symlinks against the per-source vpk cache
and leaves foreign symlinks untouched. Initialize-time guard refuses
to mount a partial overlay if any expected vpk is missing from cache.
Refresh service uses shutil.move to handle EXDEV when /tmp and the
cache live on different filesystems.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a typed-overlay model with workshop as the first non-external type:
deduplicated WorkshopItem registry, symlink-based overlay directories,
auto-rebuild after item changes, admin global refresh, and a unified
Create-overlay UI with web-managed paths.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>