feat(macos): add native ClawDnD app shell - #83
Conversation
|
Warning Review limit reached
Your plan includes 5 reviews of capacity. Refill in 11 minutes and 52 seconds. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more review capacity refills, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than trial, open-source, and free plans. In all cases, review capacity refills continuously over time. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (5)
📝 WalkthroughWalkthroughA native macOS app (ClawDnDApp) is added with domain models, AppProcessService to manage viewer/provider subprocesses, provider adapters (Claude/Codex/OpenClaw), a CampaignStore for filesystem snapshots, SwiftUI views (Play, Campaigns, Monitor, Providers, Settings, Logs), port/repo/shell utilities, build/launch scripts, and a macOS CI workflow. ChangesmacOS Native App Implementation
Sequence Diagram(s)sequenceDiagram
participant User
participant PlayView
participant AppProcessService
participant ProviderRegistry
participant ManagedProcess
PlayView->>AppProcessService: startProviderSession(kind, repoPath, world, runId, ...)
AppProcessService->>ProviderRegistry: lookup adapter -> adapter.startSession(...)
ProviderRegistry-->>AppProcessService: ProviderLaunchRequest(exec, args, env, wd)
AppProcessService->>ManagedProcess: launchManagedProcess(exec, args, env, wd)
ManagedProcess-->>AppProcessService: stream stdout/stderr -> append logs
AppProcessService-->>PlayView: return dashboard URL
PlayView->>WebView: load(dashboard URL)
User->>PlayView: Click "Stop"
PlayView->>AppProcessService: stopProvider()
AppProcessService->>ManagedProcess: terminate() -> close()
ManagedProcess-->>AppProcessService: termination handler records exit status
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related issues
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Warning Review ran into problems🔥 ProblemsErrors were encountered while retrieving linked issues. Errors (1)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 15
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@macos/ClawDnDApp/Sources/ClawDnDApp/Models/CampaignSummary.swift`:
- Around line 35-39: The dayLabel computed property currently interpolates "Day
\(day), \(timeOfDay)" which leaves a trailing comma when timeOfDay is empty;
update the dayLabel logic (property name: dayLabel, symbols: day and timeOfDay)
to only include the comma and space when timeOfDay is non-empty—e.g. build the
label by conditionally appending ", \(timeOfDay)" or join non-empty
components—so outputs become "Day 3" when timeOfDay == "" and "Day 3, Morning"
otherwise.
In `@macos/ClawDnDApp/Sources/ClawDnDApp/Models/LocalEndpoint.swift`:
- Around line 10-24: LocalEndpoint currently duplicates endpoint address state
with both a stored port and a stored url causing potential divergence; pick a
single source of truth (store url only) and derive port when needed. Remove the
stored port property, keep var url: URL as the canonical address, implement a
computed port property that parses the port from url (via URLComponents or
url.port ?? default), and update dashboardURL and monitorURL to build from url
(e.g., url with path "/dashboard" and "/monitor") instead of interpolating port;
adjust any initializers/usages to construct LocalEndpoint with a URL and ensure
Equatable/Identifiable semantics remain unchanged.
In `@macos/ClawDnDApp/Sources/ClawDnDApp/Services/AppProcessService.swift`:
- Around line 123-125: Reset provider-related state (providerProcess,
providerLog, runningProvider) before attempting to relaunch so a failed launch
cannot leave stale values; specifically, update the block that calls
providerProcess?.terminate() and then calls launchManagedProcess(...) to also
clear runningProvider (and any other provider state) prior to the try, and apply
the same reset in the analogous block around the code referenced at the second
occurrence (the code around lines 133-135) so both failure paths cannot retain
an old runningProvider value.
- Around line 184-191: Termination handler currently closes the managed handle
and logs the exit but leaves stale process references; update the
terminationHandler closure (the Task `@MainActor` block inside
process.terminationHandler) to also clear the owned process state by setting
providerProcess/viewerProcess and runningProvider/runningViewer to nil as
appropriate based on the stream value, ensuring these mutations occur on the
MainActor after calling managed?.close() and before/after append(...) so no
stale references remain after natural or crash exits.
- Around line 205-212: The append(_ text: String, stream: LogStream, prefix:
String? = nil) method currently appends to supervisorLog and providerLog with no
bounds; introduce a hard cap (e.g. MAX_LOG_CHARS or MAX_LOG_LINES constant) and,
after building the new line, trim the target string (supervisorLog or
providerLog) to that cap by dropping the oldest characters or lines so the
newest content is preserved; update append to enforce this cap for both
.supervisor and .provider paths and ensure trimming logic keeps valid line
boundaries (and consider using a small helper trimLog(_:, toMaxChars:) to
centralize behavior).
In `@macos/ClawDnDApp/Sources/ClawDnDApp/Services/CampaignStore.swift`:
- Around line 3-24: The reload method on the `@MainActor` CampaignStore is
performing blocking disk I/O (loadCampaigns, directory walking and JSON reads)
on the main actor; change reload to perform the file system work off-main-thread
(e.g., make reload async and call loadCampaigns from a background Task or
Task.detached or DispatchQueue.global) and then marshal results back to the main
actor to update `@Published` properties (campaigns, lastError). Specifically, keep
loadCampaigns usage but call it from a background context, catch errors there,
and ensure the assignment campaigns = ... and lastError = ... happens on the
MainActor (or via await MainActor.run) so UI updates remain safe.
- Around line 21-23: The catch in CampaignStore.reload currently only sets
lastError, leaving stale campaigns visible; update the catch block in reload()
to also clear the in-memory campaign collection (e.g., set campaigns = [] or
call campaigns.removeAll()) and update any related state flags (like isLoading =
false) so the UI no longer shows stale campaign data after a failed reload;
ensure you still set lastError = error.localizedDescription.
In `@macos/ClawDnDApp/Sources/ClawDnDApp/Services/PortFinder.swift`:
- Around line 5-20: The function firstFreePort currently returns the original
start value when no free port is found, which can be occupied; change
firstFreePort(preferredPort:) to signal failure explicitly by returning an
optional Int (Int?) or throwing an error instead of returning start. Update the
implementation (references: firstFreePort, isAvailable, preferredPort, start,
nearbyEnd) so that after exhausting the nearby range and fallback range it
returns nil (or throws a descriptive PortNotFoundError) rather than returning
start, and update all callers to handle the new optional/throwing contract.
In `@macos/ClawDnDApp/Sources/ClawDnDApp/Services/ProviderAdapters.swift`:
- Around line 240-242: The adapter(for kind: ProviderKind) method currently
returns a ClaudeProvider() when adapters[kind] is missing; change this to fail
fast instead of silently falling back: lookup adapters[kind] and if missing call
fatalError (or assert) with a clear message including the missing ProviderKind
(e.g. "Unmapped ProviderKind: \(kind)"), otherwise return the found adapter;
update references to adapters and ClaudeProvider in this method only.
In `@macos/ClawDnDApp/Sources/ClawDnDApp/Services/RepositoryLocator.swift`:
- Around line 5-7: The CLAWDND_REPO_ROOT environment value is not being
expanded, so paths starting with "~" fail looksLikeRepo; in
RepositoryLocator.swift where you read
ProcessInfo.processInfo.environment["CLAWDND_REPO_ROOT"], expand the tilde
before validation (e.g. use NSString(string: env).expandingTildeInPath or
similar) and then pass the expandedPath to looksLikeRepo and return the expanded
path instead of the raw env; keep the check against looksLikeRepo and update any
returned value to the expanded form.
- Around line 19-30: The code in RepositoryLocator.swift currently falls back to
the hardcoded lexar path (variable lexar) and returns it even when
looksLikeRepo(lexar) and looksLikeRepo(cwd) are both false; remove that
machine-specific fallback by deleting the final "return lexar.path" and instead
have the function return an explicit failure (e.g., make the function return an
optional or throw an error) when neither lexar nor cwd pass looksLikeRepo;
update callers to handle the nil/throwing behavior so they stop relying on the
hardcoded lexar value (refer to the symbols lexar, cwd, looksLikeRepo and the
repository-locating function in RepositoryLocator.swift).
In `@macos/ClawDnDApp/Sources/ClawDnDApp/Views/MonitorView.swift`:
- Around line 43-55: The port fallback chain in openMonitor() is unnecessarily
complex; simplify by using the port that startViewer guarantees to set: read
processService.viewerEndpoint?.port (or parse the returned dashboard URL if you
prefer) and use that single source to build webURL, removing the fallback to
preferredPort and the URLComponents parse chain; update the openMonitor() logic
to set port = processService.viewerEndpoint!.port (or safely unwrap) and then
webURL = URL(string: "http://127.0.0.1:\(port)/monitor").
In `@macos/ClawDnDApp/Sources/ClawDnDApp/Views/PlayView.swift`:
- Around line 169-173: The newRunID() function currently allocates a new
DateFormatter on every call; replace that with a single shared DateFormatter
instance (e.g. a static property) to avoid repeated expensive
initializations—move the DateFormatter creation out of newRunID() into a static
let (configured once with dateFormat "yyyyMMdd-HHmmss" and appropriate
locale/timeZone if needed) and have newRunID() use that shared formatter to
produce the string.
In `@macos/ClawDnDApp/Sources/ClawDnDApp/Views/SettingsView.swift`:
- Around line 17-55: The form currently accepts arbitrary input for repoPath,
stateDir, preferredPort, budget, sessionBudget, maxTurns, codexProviderCommand,
openClawProviderCommand, etc., with no user feedback; add inline validation
logic in SettingsView to validate each field (e.g., use
FileManager.default.fileExists for repoPath/stateDir, ensure preferredPort and
budget/sessionBudget/maxTurns parse to Int and fall within expected ranges, and
verify command fields are non-empty or point to executables) and expose
per-field error strings (e.g., repoPathError, portError, budgetError) that are
updated in onChange handlers; show those error messages as small red Text views
under the corresponding TextField and visually indicate invalid fields (red
border/foreground or .overlay), and disable or prevent saving/applying settings
until all error strings are empty.
In `@macos/ClawDnDApp/Sources/ClawDnDApp/Views/WebView.swift`:
- Around line 15-20: The WebView lacks a WKNavigationDelegate for error
handling; create a Coordinator class that implements WKNavigationDelegate
(implement webView(_:didFailProvisionalNavigation:withError:) and
webView(_:didFail:withError:)), set view.navigationDelegate =
context.coordinator inside updateNSView(_ view: WKWebView, context: Context),
and surface any navigation errors through a callback or Binding (e.g., an
onError closure or Binding<Error?>) so the parent MonitorView/PlayView can
display diagnostic UI when loading fails.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: efe649f2-b35f-4019-9894-60cb74901c9c
📒 Files selected for processing (32)
.codex/environments/environment.toml.gitignoreclawdnd-play.commandmacos/ClawDnDApp/Package.swiftmacos/ClawDnDApp/RELEASE_CHECKLIST.mdmacos/ClawDnDApp/Sources/ClawDnDApp/App/ClawDnDApp.swiftmacos/ClawDnDApp/Sources/ClawDnDApp/Models/AppSection.swiftmacos/ClawDnDApp/Sources/ClawDnDApp/Models/CampaignSummary.swiftmacos/ClawDnDApp/Sources/ClawDnDApp/Models/DependencyStatus.swiftmacos/ClawDnDApp/Sources/ClawDnDApp/Models/LocalEndpoint.swiftmacos/ClawDnDApp/Sources/ClawDnDApp/Models/ProviderModels.swiftmacos/ClawDnDApp/Sources/ClawDnDApp/Services/AppProcessService.swiftmacos/ClawDnDApp/Sources/ClawDnDApp/Services/CampaignStore.swiftmacos/ClawDnDApp/Sources/ClawDnDApp/Services/DependencyChecker.swiftmacos/ClawDnDApp/Sources/ClawDnDApp/Services/Diagnostics.swiftmacos/ClawDnDApp/Sources/ClawDnDApp/Services/PortFinder.swiftmacos/ClawDnDApp/Sources/ClawDnDApp/Services/ProviderAdapters.swiftmacos/ClawDnDApp/Sources/ClawDnDApp/Services/RepositoryLocator.swiftmacos/ClawDnDApp/Sources/ClawDnDApp/Services/Shell.swiftmacos/ClawDnDApp/Sources/ClawDnDApp/Views/CampaignsView.swiftmacos/ClawDnDApp/Sources/ClawDnDApp/Views/EmptyStateView.swiftmacos/ClawDnDApp/Sources/ClawDnDApp/Views/LogsView.swiftmacos/ClawDnDApp/Sources/ClawDnDApp/Views/MonitorView.swiftmacos/ClawDnDApp/Sources/ClawDnDApp/Views/PlayView.swiftmacos/ClawDnDApp/Sources/ClawDnDApp/Views/ProvidersView.swiftmacos/ClawDnDApp/Sources/ClawDnDApp/Views/RootView.swiftmacos/ClawDnDApp/Sources/ClawDnDApp/Views/SettingsView.swiftmacos/ClawDnDApp/Sources/ClawDnDApp/Views/WebView.swiftscript/build_and_run.shscripts/launch_common.shscripts/play.shscripts/play_party.sh
📜 Review details
🧰 Additional context used
🧬 Code graph analysis (12)
macos/ClawDnDApp/Sources/ClawDnDApp/App/ClawDnDApp.swift (1)
macos/ClawDnDApp/Sources/ClawDnDApp/Services/Diagnostics.swift (1)
copy(5-10)
macos/ClawDnDApp/Sources/ClawDnDApp/Services/DependencyChecker.swift (1)
macos/ClawDnDApp/Sources/ClawDnDApp/Services/Shell.swift (1)
which(4-22)
macos/ClawDnDApp/Sources/ClawDnDApp/Views/ProvidersView.swift (1)
macos/ClawDnDApp/Sources/ClawDnDApp/Services/AppProcessService.swift (1)
providerStatuses(41-43)
macos/ClawDnDApp/Sources/ClawDnDApp/Views/MonitorView.swift (2)
macos/ClawDnDApp/Sources/ClawDnDApp/Services/AppProcessService.swift (1)
startViewer(45-87)macos/ClawDnDApp/Sources/ClawDnDApp/Views/PlayView.swift (1)
startViewer(122-132)
macos/ClawDnDApp/Sources/ClawDnDApp/Views/LogsView.swift (1)
macos/ClawDnDApp/Sources/ClawDnDApp/Services/Diagnostics.swift (1)
copy(5-10)
scripts/play.sh (1)
scripts/launch_common.sh (2)
clawdnd_missing_commands(7-19)clawdnd_choose_port(46-78)
scripts/play_party.sh (1)
scripts/launch_common.sh (2)
clawdnd_missing_commands(7-19)clawdnd_choose_port(46-78)
macos/ClawDnDApp/Sources/ClawDnDApp/Services/AppProcessService.swift (6)
macos/ClawDnDApp/Sources/ClawDnDApp/Services/ProviderAdapters.swift (3)
detectAll(244-246)adapter(240-242)startSession(48-73)macos/ClawDnDApp/Sources/ClawDnDApp/Views/PlayView.swift (1)
startViewer(122-132)macos/ClawDnDApp/Sources/ClawDnDApp/Services/Shell.swift (1)
which(4-22)macos/ClawDnDApp/Sources/ClawDnDApp/Services/RepositoryLocator.swift (1)
looksLikeRepo(32-37)macos/ClawDnDApp/Sources/ClawDnDApp/Services/PortFinder.swift (1)
firstFreePort(5-20)macos/ClawDnDApp/Sources/ClawDnDApp/Services/DependencyChecker.swift (1)
check(12-16)
macos/ClawDnDApp/Sources/ClawDnDApp/Views/RootView.swift (3)
macos/ClawDnDApp/Sources/ClawDnDApp/Services/AppProcessService.swift (1)
refreshDependencies(37-39)macos/ClawDnDApp/Sources/ClawDnDApp/Services/RepositoryLocator.swift (1)
defaultRepoPath(4-30)macos/ClawDnDApp/Sources/ClawDnDApp/Services/CampaignStore.swift (1)
reload(8-24)
macos/ClawDnDApp/Sources/ClawDnDApp/Views/CampaignsView.swift (2)
macos/ClawDnDApp/Sources/ClawDnDApp/Services/AppProcessService.swift (1)
startViewer(45-87)macos/ClawDnDApp/Sources/ClawDnDApp/Services/CampaignStore.swift (1)
reload(8-24)
macos/ClawDnDApp/Sources/ClawDnDApp/Services/ProviderAdapters.swift (2)
macos/ClawDnDApp/Sources/ClawDnDApp/Services/AppProcessService.swift (1)
append(205-213)macos/ClawDnDApp/Sources/ClawDnDApp/Services/Shell.swift (2)
fileExists(24-26)which(4-22)
macos/ClawDnDApp/Sources/ClawDnDApp/Views/PlayView.swift (1)
macos/ClawDnDApp/Sources/ClawDnDApp/Services/AppProcessService.swift (4)
startViewer(45-87)stopProvider(147-151)stopViewer(89-96)startProviderSession(98-145)
🪛 SwiftLint (0.63.2)
macos/ClawDnDApp/Sources/ClawDnDApp/App/ClawDnDApp.swift
[Warning] 29-29: Classes should have an explicit deinit method
(required_deinit)
macos/ClawDnDApp/Sources/ClawDnDApp/Services/AppProcessService.swift
[Warning] 178-178: Prefer failable String(bytes:encoding:) initializer when converting Data to String
(optional_data_string_conversion)
[Warning] 4-4: Classes should have an explicit deinit method
(required_deinit)
[Warning] 227-227: Classes should have an explicit deinit method
(required_deinit)
macos/ClawDnDApp/Sources/ClawDnDApp/Services/CampaignStore.swift
[Warning] 4-4: Classes should have an explicit deinit method
(required_deinit)
🔍 Remote MCP
It appears the repository is private and I cannot access it via the DeepWiki tools. Let me provide a comprehensive summary based on the detailed PR context you've provided:
PR Review Summary: Native macOS App Shell for ClawDnD
Based on the comprehensive pull request changes provided, here are the key architectural and technical findings relevant to your review:
Architectural Overview
The PR introduces a non-authoritative orchestrator pattern: the native macOS app acts as a UI control surface and read-only monitor without modifying game state. This is explicitly stated in the PR objectives—"the native app acts as an orchestrator and read surface (not a second game-state writer)." The Python viewer (viewer/server.py) retains authority over all game state, while the Swift app:
- Launches and monitors external processes (viewer + providers)
- Browses campaigns from filesystem snapshots
- Provides UI for configuration and status monitoring
Key Service Components & Responsibilities
AppProcessService (@MainActor): Central orchestrator managing:
- Viewer lifecycle: validates repo path, checks
python3availability, selects free port, launchespython3 viewer/server.pywith optional state directory injection - Provider session management: validates config, builds launch parameters via adapters, manages process I/O streaming with real-time log capture
- Error tracking and diagnostic log aggregation (with a
diagnosticsproperty exposed for pasteboard copying)
CampaignStore: Read-only campaign discovery and aggregation:
- Loads from two roots (
play-stateandqa/state) - Parses
snapshot.jsonfiles (extracting id, title, world_id, day, time_of_day, location, party) - Infers provider type based on file presence (e.g.,
companion_0.mcp.json→ "Claude party",dm.mcp.json→ "Claude", otherwise "Local") - Computes campaign recency by comparing snapshot modification date to latest
sessions/*.jsonltimestamp - Considers campaigns "live" if snapshot is < 120 seconds old
Provider Adapters Pattern: Three adapter implementations for Claude, Codex, and OpenClaw:
- detect(): checks for CLI tools, config files, or user-configured launch commands
- startSession(): builds launch requests with provider-specific environment variables and validates required configuration
- Returns structured
ProviderStatus(availability states: installed, configured, missing, error)
Helper Services:
DependencyChecker: static list of required commands → runs/usr/bin/whichfor eachPortFinder: clamps preferred port to 1–65535, tests availability via socket bind on 127.0.0.1, falls back to scanning nearby/default rangesRepositoryLocator: checksCLAWDND_REPO_ROOTenv var, walks up from app bundle directory, falls back to hardcoded path; validates via presence ofviewer/server.pyand.claude-plugin/plugin.jsonShell: wraps/usr/bin/whichandFileManager.fileExistswith tilde expansion
Campaign Browsing & State Authority
Campaign data is read-only from filesystem:
- Two isolated state roots (
play-state/for production runs,qa/state/for QA) - Discovery walks run directories → per-campaign directories →
snapshot.jsonper campaign - Party/provider inference happens locally; no API calls to game state
- Last update computed from filesystem timestamps, not from viewer endpoints
Process Management & I/O
ManagedProcess wrapper (used by AppProcessService):
- Pipes stdout/stderr to a shared pipe with readability handler
- Streams log output to background queue, dispatches log appends back to
@MainActor - Installs termination handler that closes file descriptors, records exit status, clears provider state
- Provides
terminate()andclose()helpers for cleanup
Build & Launch Infrastructure
build_and_run.sh script:
- Derives project paths relative to its location
- Compiles via
swift build, copies binary into.appbundle structure - Writes
Info.plistvia heredoc - Attempts codesign when available
- Supports modes:
run,--verify,--debug,--logs,--telemetry,--release-check
launch_common.sh (new bash helpers):
clawdnd_missing_commands: validates required tools (python3, claude, uv, jq, curl)clawdnd_port_available: checks TCP port bindability via Python socket probeclawdnd_choose_port: validates/selects available port or errors on explicit request
Script modifications (play.sh, play_party.sh):
- Now optionally source
launch_common.shfor dependency/port validation - Preserve user-supplied vs. defaulted port arguments so play.sh can fall back appropriately
- Track
PORT_EXPLICITflag to distinguish user intent from internal defaults
Configuration & UI Design
RootView architecture:
- NavigationSplitView with sidebar + tabbed detail area
- SidebarView lists AppSection cases (play, campaigns, monitor, providers, settings, logs)
- StatusStrip shows compact status: viewer endpoint/port, state directory, active campaign, running provider, last error (if any)
- AppStorage bindings persist: repo path, provider/world/config commands, budget controls, voice backend
SettingsView: Grouped form with editable fields for workspace (repo path, state dir, port), defaults (world, provider, voice backend), budgets, and provider commands.
Code Organization & Reusability
- Model types are value semantics (structs) with computed properties for derived labels (e.g.,
CampaignSummary.sourceLabel,dayLabel,partyLabel) - Views are compositional (EmptyStateView, ProviderCard, CampaignRow, CampaignDetail, LogText)
- Environment injection: AppProcessService wired as
@EnvironmentObject; child views receive via@EnvironmentObjector@Binding
🔇 Additional comments (40)
macos/ClawDnDApp/Sources/ClawDnDApp/Models/AppSection.swift (1)
3-34: LGTM!macos/ClawDnDApp/Sources/ClawDnDApp/Models/CampaignSummary.swift (1)
3-33: LGTM!macos/ClawDnDApp/Sources/ClawDnDApp/Models/DependencyStatus.swift (1)
3-10: LGTM!macos/ClawDnDApp/Sources/ClawDnDApp/Models/LocalEndpoint.swift (1)
3-8: LGTM!macos/ClawDnDApp/Sources/ClawDnDApp/Models/ProviderModels.swift (1)
3-81: LGTM!macos/ClawDnDApp/Sources/ClawDnDApp/Services/PortFinder.swift (1)
22-40: LGTM!macos/ClawDnDApp/Sources/ClawDnDApp/Services/Shell.swift (1)
3-27: LGTM!macos/ClawDnDApp/Sources/ClawDnDApp/Services/RepositoryLocator.swift (1)
32-37: LGTM!macos/ClawDnDApp/Sources/ClawDnDApp/Services/DependencyChecker.swift (1)
3-17: LGTM!macos/ClawDnDApp/Sources/ClawDnDApp/Services/Diagnostics.swift (1)
4-10: LGTM!macos/ClawDnDApp/Package.swift (1)
1-16: LGTM!script/build_and_run.sh (1)
1-123: LGTM!scripts/launch_common.sh (1)
1-78: LGTM!scripts/play.sh (1)
27-34: LGTM!Also applies to: 42-46
scripts/play_party.sh (1)
49-56: LGTM!Also applies to: 60-61, 74-82, 93-95
.codex/environments/environment.toml (1)
1-11: LGTM!.gitignore (1)
38-42: LGTM!clawdnd-play.command (1)
28-40: LGTM!macos/ClawDnDApp/RELEASE_CHECKLIST.md (1)
1-32: LGTM!macos/ClawDnDApp/Sources/ClawDnDApp/App/ClawDnDApp.swift (2)
29-34: SwiftLint false positive: deinit not required.The static analysis warning about missing
deinitcan be ignored.NSApplicationDelegateinstances managed by SwiftUI don't require explicit deinitialization unless they hold unmanaged resources or observers.
4-27: LGTM!macos/ClawDnDApp/Sources/ClawDnDApp/Views/RootView.swift (3)
3-100: LGTM!
102-118: LGTM!
120-153: LGTM!macos/ClawDnDApp/Sources/ClawDnDApp/Views/CampaignsView.swift (3)
3-85: LGTM!
87-116: LGTM!
118-177: LGTM!macos/ClawDnDApp/Sources/ClawDnDApp/Views/EmptyStateView.swift (1)
3-18: LGTM!macos/ClawDnDApp/Sources/ClawDnDApp/Views/LogsView.swift (2)
3-35: LGTM!
37-51: LGTM!macos/ClawDnDApp/Sources/ClawDnDApp/Views/MonitorView.swift (1)
57-59: LGTM!macos/ClawDnDApp/Sources/ClawDnDApp/Views/PlayView.swift (4)
3-120: LGTM!
122-132: LGTM!
134-150: LGTM!
152-167: LGTM!macos/ClawDnDApp/Sources/ClawDnDApp/Views/ProvidersView.swift (2)
3-81: LGTM!
83-124: LGTM!macos/ClawDnDApp/Sources/ClawDnDApp/Views/SettingsView.swift (2)
3-14: LGTM!
42-46: ⚡ Quick winValidate numeric budget inputs instead of keeping them as free-form
StringsIn
macos/ClawDnDApp/Sources/ClawDnDApp/Views/SettingsView.swift(“Caps”, lines 42-46),budget,sessionBudget, andmaxTurnsare edited asString. Add explicit numeric parsing/validation right before these values are used (e.g., constructing provider args/env or invoking scripts), or switch the bindings to numeric types and useTextField(value:format:).Example parsing guard:
let trimmed = input.trimmingCharacters(in: .whitespacesAndNewlines) guard let v = Int(trimmed), v >= 0 else { return nil } // handle invalid input explicitlymacos/ClawDnDApp/Sources/ClawDnDApp/Views/WebView.swift (1)
4-13: LGTM!
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
macos/ClawDnDApp/Sources/ClawDnDApp/Views/MonitorView.swift (1)
45-50:⚠️ Potential issue | 🟠 Major | ⚡ Quick winDo not fall back to the dashboard URL in the Monitor tab.
If
viewerEndpointis unavailable, Line 50 opens/dashboard, so this screen can render the wrong surface after a successful viewer start. Derive/monitorfrom the returned URL instead of reusing the dashboard URL.Proposed fix
let dashboard = try processService.startViewer( repoPath: repoPath, preferredPort: preferredPort, stateDir: stateDir ) - webURL = processService.viewerEndpoint?.monitorURL ?? dashboard + webURL = processService.viewerEndpoint?.monitorURL + ?? dashboard.deletingLastPathComponent().appendingPathComponent("monitor")🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@macos/ClawDnDApp/Sources/ClawDnDApp/Views/MonitorView.swift` around lines 45 - 50, The Monitor tab currently falls back to the dashboard URL when processService.viewerEndpoint is nil, causing the Monitor view to render the wrong surface; update the assignment after calling processService.startViewer to derive a /monitor URL from the returned dashboard URL when viewerEndpoint or viewerEndpoint.monitorURL is unavailable: call processService.startViewer(...) as before, then if processService.viewerEndpoint?.monitorURL exists use it, otherwise parse the returned dashboard string (variable dashboard) and replace or append its path with "/monitor" to compute webURL (referencing startViewer, dashboard, viewerEndpoint, monitorURL, and webURL).
♻️ Duplicate comments (1)
macos/ClawDnDApp/Sources/ClawDnDApp/Services/RepositoryLocator.swift (1)
4-32:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftReturn an explicit failure instead of
"".Line 32 still encodes repo-discovery failure in-band. That leaves every caller responsible for remembering a special case, instead of forcing launch/browse flows to stop when no valid repo was found. Make
defaultRepoPath()returnString?or throw, and handle that failure explicitly at the call sites.Suggested direction
-enum RepositoryLocator { - static func defaultRepoPath() -> String { +enum RepositoryLocator { + static func defaultRepoPath() -> String? { @@ - return expanded + return expanded } } @@ - return homeRepo.path + return homeRepo.path } @@ - return cwd.path + return cwd.path } - return "" + return nil } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@macos/ClawDnDApp/Sources/ClawDnDApp/Services/RepositoryLocator.swift` around lines 4 - 32, The function defaultRepoPath() currently returns an empty string on failure; change its signature to return an optional String? (or alternately make it throwing) so failure is explicit, update its implementation to return nil (or throw a descriptive error) instead of "" when no repo is found, and update all callers (places that call RepositoryLocator.defaultRepoPath()) to handle the nil/throwing case explicitly (e.g., show an error, abort launch, or present a repo-browse UI) rather than checking for "" — keep the repo detection logic (looksLikeRepo, bundleURL/cursor loop, homeRepo, cwd) unchanged except for the new return values and ensure unit/usage sites compile and handle the new failure path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@macos/ClawDnDApp/Sources/ClawDnDApp/Services/AppProcessService.swift`:
- Around line 144-149: The provider-owned viewerEndpoint set in
startProviderSession must be cleared/marked stopped when the provider stops or
crashes; update stopProvider() and the provider termination handler to reset
viewerEndpoint (the LocalEndpoint instance used for "Provider viewer") by
setting its status to .stopped and clearing or resetting any provider-specific
URL/healthPath so the UI no longer targets a dead localhost URL; ensure the same
reset is applied in all termination paths that currently mirror
startProviderSession changes.
- Around line 233-238: The trimLogIfNeeded implementation can drop the entire
retained buffer when the suffix contains a single oversized line because it
always removes up to the first newline even if none exists; update
trimLogIfNeeded to first truncate to maxLogCharacters, then only remove the
leading partial line if there is a newline in the retained suffix (i.e. check
for log.firstIndex(of: "\n") and remove through that newline only when it
exists), otherwise keep the truncated partial line so the newest output is
preserved.
In `@macos/ClawDnDApp/Sources/ClawDnDApp/Services/CampaignStore.swift`:
- Around line 8-31: Concurrent detached Tasks started by reload(repoPath:) can
finish out of order and overwrite state; change reload(repoPath:) to track the
active reload (e.g., an instance property like currentReloadTask: Task<Void,
Never>? or a reloadToken/Int) and on each call cancel the previous Task or
increment the token before starting a new Task, then ensure the Task checks for
cancellation (Task.isCancelled) or verifies the token before calling
finishReload(campaigns:lastError:); this guarantees older scans are ignored and
only the latest reload updates campaigns/lastError.
In `@macos/ClawDnDApp/Sources/ClawDnDApp/Views/PlayView.swift`:
- Around line 169-176: The runIDFormatter DateFormatter can produce different
results under user locales/calendars; update the static runIDFormatter
initializer (used by newRunID) to set formatter.locale = Locale(identifier:
"en_US_POSIX") and formatter.calendar = Calendar(identifier: .gregorian) (and
optionally formatter.timeZone = TimeZone(secondsFromGMT: 0)) so the fixed-format
run IDs are stable across user settings.
---
Outside diff comments:
In `@macos/ClawDnDApp/Sources/ClawDnDApp/Views/MonitorView.swift`:
- Around line 45-50: The Monitor tab currently falls back to the dashboard URL
when processService.viewerEndpoint is nil, causing the Monitor view to render
the wrong surface; update the assignment after calling
processService.startViewer to derive a /monitor URL from the returned dashboard
URL when viewerEndpoint or viewerEndpoint.monitorURL is unavailable: call
processService.startViewer(...) as before, then if
processService.viewerEndpoint?.monitorURL exists use it, otherwise parse the
returned dashboard string (variable dashboard) and replace or append its path
with "/monitor" to compute webURL (referencing startViewer, dashboard,
viewerEndpoint, monitorURL, and webURL).
---
Duplicate comments:
In `@macos/ClawDnDApp/Sources/ClawDnDApp/Services/RepositoryLocator.swift`:
- Around line 4-32: The function defaultRepoPath() currently returns an empty
string on failure; change its signature to return an optional String? (or
alternately make it throwing) so failure is explicit, update its implementation
to return nil (or throw a descriptive error) instead of "" when no repo is
found, and update all callers (places that call
RepositoryLocator.defaultRepoPath()) to handle the nil/throwing case explicitly
(e.g., show an error, abort launch, or present a repo-browse UI) rather than
checking for "" — keep the repo detection logic (looksLikeRepo, bundleURL/cursor
loop, homeRepo, cwd) unchanged except for the new return values and ensure
unit/usage sites compile and handle the new failure path.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: fccd817e-1a69-4157-b262-8f2207130dc4
📒 Files selected for processing (9)
macos/ClawDnDApp/Sources/ClawDnDApp/Models/CampaignSummary.swiftmacos/ClawDnDApp/Sources/ClawDnDApp/Models/LocalEndpoint.swiftmacos/ClawDnDApp/Sources/ClawDnDApp/Services/AppProcessService.swiftmacos/ClawDnDApp/Sources/ClawDnDApp/Services/CampaignStore.swiftmacos/ClawDnDApp/Sources/ClawDnDApp/Services/PortFinder.swiftmacos/ClawDnDApp/Sources/ClawDnDApp/Services/ProviderAdapters.swiftmacos/ClawDnDApp/Sources/ClawDnDApp/Services/RepositoryLocator.swiftmacos/ClawDnDApp/Sources/ClawDnDApp/Views/MonitorView.swiftmacos/ClawDnDApp/Sources/ClawDnDApp/Views/PlayView.swift
📜 Review details
🧰 Additional context used
🧬 Code graph analysis (1)
macos/ClawDnDApp/Sources/ClawDnDApp/Services/AppProcessService.swift (1)
macos/ClawDnDApp/Sources/ClawDnDApp/Services/PortFinder.swift (1)
firstFreePort(5-20)
There was a problem hiding this comment.
♻️ Duplicate comments (1)
macos/ClawDnDApp/Sources/ClawDnDApp/Services/AppProcessService.swift (1)
128-132:⚠️ Potential issue | 🟠 Major | ⚡ Quick winClear provider viewer endpoint before relaunch attempt.
If
startProviderSessionterminates an old provider and the new launch fails,viewerEndpointcan remain as stale"Provider viewer"because ownership is nulled before the old termination handler can apply cleanup.Proposed fix
providerProcess?.terminate() providerProcess = nil runningProvider = nil + stopProviderViewerEndpointIfNeeded() providerLog = "" let managed = try launchManagedProcess(Also applies to: 154-159
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@macos/ClawDnDApp/Sources/ClawDnDApp/Services/AppProcessService.swift` around lines 128 - 132, When stopping an existing provider in startProviderSession, clear the stale viewerEndpoint immediately (set viewerEndpoint = nil or empty string) before nulling providerProcess/runningProvider and before attempting the new launch so a failed relaunch cannot leave the old "Provider viewer" value; apply the same fix to the other shutdown block (the code around the second provider termination at lines 154-159) so both termination paths explicitly clear viewerEndpoint prior to restart/cleanup. Ensure you update references to viewerEndpoint in AppProcessService (the startProviderSession termination logic and the second termination block) rather than relying on the old termination handler to do it.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Duplicate comments:
In `@macos/ClawDnDApp/Sources/ClawDnDApp/Services/AppProcessService.swift`:
- Around line 128-132: When stopping an existing provider in
startProviderSession, clear the stale viewerEndpoint immediately (set
viewerEndpoint = nil or empty string) before nulling
providerProcess/runningProvider and before attempting the new launch so a failed
relaunch cannot leave the old "Provider viewer" value; apply the same fix to the
other shutdown block (the code around the second provider termination at lines
154-159) so both termination paths explicitly clear viewerEndpoint prior to
restart/cleanup. Ensure you update references to viewerEndpoint in
AppProcessService (the startProviderSession termination logic and the second
termination block) rather than relying on the old termination handler to do it.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: cf9bddbf-b056-4d72-9443-78f373437832
📒 Files selected for processing (6)
macos/ClawDnDApp/Sources/ClawDnDApp/Services/AppProcessService.swiftmacos/ClawDnDApp/Sources/ClawDnDApp/Services/CampaignStore.swiftmacos/ClawDnDApp/Sources/ClawDnDApp/Services/RepositoryLocator.swiftmacos/ClawDnDApp/Sources/ClawDnDApp/Views/MonitorView.swiftmacos/ClawDnDApp/Sources/ClawDnDApp/Views/PlayView.swiftmacos/ClawDnDApp/Sources/ClawDnDApp/Views/RootView.swift
📜 Review details
🧰 Additional context used
🧬 Code graph analysis (1)
macos/ClawDnDApp/Sources/ClawDnDApp/Views/RootView.swift (1)
macos/ClawDnDApp/Sources/ClawDnDApp/Services/RepositoryLocator.swift (1)
defaultRepoPath(4-33)
🔍 Remote MCP
The DeepWiki tools require authentication for this repository. However, based on the comprehensive AI summary provided in the context, I have sufficient information to provide a focused review summary. Let me synthesize the key architectural findings from the PR changes:
Additional Context Summary for PR Review
Based on analysis of the pulled code changes, here are the key architectural and implementation details relevant for effective review:
Core Architecture Pattern
The PR establishes a clear separation of concerns: the native macOS app acts as an orchestrator and read-only UI surface rather than a second writer to game state. This is critical because:
AppProcessServicemanages subprocess lifecycles for the viewer (Python-based dashboard server) and providers (external processes)CampaignStorereads campaign snapshots from filesystem (play-state/andqa/state/) without writing state- The app delegates all game logic to the existing ClawDnD engine via the viewer and provider processes
Subprocess Management Implementation
AppProcessService implements a unified subprocess handling pattern via:
launchManagedProcess()centralizes all process lifecycle: environment merging, stdout/stderr piping, asynchronous readability handlers, and termination cleanup- Logs are trimmed to a
maxLogCharacterslimit and appended per-stream (supervisor vs provider logs) - Termination handlers distinguish between viewer/provider processes and clean up corresponding state
- Port discovery via
PortFindervalidates port availability through TCP socket binding on127.0.0.1
Provider Adapter Layer
Three provider implementations share a common contract:
- ClaudeProvider: Validates Claude CLI presence via
Shell.which(), sources environment frombudgetEnvironmenthelper - CodexProvider & OpenClawProvider: Check user-configured command presence first, throw
ProviderError.missingDependencyorProviderError.configurationas appropriate - All adapters construct
ProviderLaunchRequestwith provider-specific environment variables (budget, world, run ID, companions, port) viaproviderEnvironmenthelper ProviderRegistryprovides a factory interface mappingProviderKindto adapters anddetectAll()for status refresh
Campaign Discovery and Metadata Extraction
CampaignStore.loadSnapshot() implements careful metadata extraction:
- Requires campaign
idfield; derivesrunIDfrom parent directory - Computes
lastUpdateas maximum of snapshot modification time and anysessions/*.jsonltimestamps - Sets
isLiveflag based on snapshot age (< 120 seconds) - Derives
locationandpartyby traversing nested JSON structures in snapshot - Provider inference (
inferProvider()) probes for companion/DM MCP config files to distinguish Claude party vs Claude vs Local
UI State Management
The root view architecture uses:
@AppStoragefor persistent settings (repo path, provider commands, budgets, voice backend)@EnvironmentObjectto injectAppProcessServiceinto the view hierarchy- Tab/view switching drives different use-cases: Play (provider + viewer), Campaigns (read-only), Monitor (dashboard), Providers (status/config), Settings, Logs
Build and Signing
script/build_and_run.shautomates app bundle creation, codesign verification, and release verification steps.codex/environments/environment.tomldefines a Codex action to run the build scriptRELEASE_CHECKLIST.mddocuments Developer ID signing, hardened runtime, notarization, and Gatekeeper requirements
Process Validation and Error Handling
- Repository location validation requires both
viewer/server.pyand.claude-plugin/plugin.jsonto exist - Dependency checking validates presence of external commands (
python3,claude,uv,jq,curl) with human-readable labels - Startup errors are caught and surfaced via alert dialogs with localized descriptions
🔇 Additional comments (6)
macos/ClawDnDApp/Sources/ClawDnDApp/Views/MonitorView.swift (1)
50-51: LGTM!macos/ClawDnDApp/Sources/ClawDnDApp/Services/RepositoryLocator.swift (1)
4-9: LGTM!Also applies to: 32-32
macos/ClawDnDApp/Sources/ClawDnDApp/Views/RootView.swift (1)
7-7: LGTM!macos/ClawDnDApp/Sources/ClawDnDApp/Views/PlayView.swift (1)
171-172: LGTM!macos/ClawDnDApp/Sources/ClawDnDApp/Services/CampaignStore.swift (1)
8-13: LGTM!Also applies to: 24-28
macos/ClawDnDApp/Sources/ClawDnDApp/Services/AppProcessService.swift (1)
192-208: LGTM!Also applies to: 235-250
Merge stacked settings validation follow-up into macos/native-app-shell.
Merge stacked Swift CI follow-up into macos/native-app-shell.
Merge stacked WebView load failure reporting follow-up into macos/native-app-shell.
|
OWNER STEER (2026-07-22 ~21:30) — this spike is now THE demo blocker, elevated above all render/paint work. Verbatim intent: the character can still walk through tables/walls and hits invisible objects in EVERY room; ~98% of painted area disagrees with collision boundaries; the constant silhouette firing on invisible proxies is the same registration error made visible. Paint-first rooms that fail navigation-truth are to be THROWN AWAY, not patched, unless greyboxes can be reverse-engineered from the paint to ≥99% agreement. Owner authorizes: workflows/ultracode, buying tools, using the owned Unity software stack — whatever it takes. REFRAME: the walk gates verify GRID-truth (clicks resolve, engine blocks cells); the unsolved problem is FELT-truth (grid ↔ painting registration per object). §9 G4 is gated on THIS, not on render polish. Execution starts now: (1) 3D-first crypt scene on the box (collision by construction) + painterly post, panel vs the 8.3 plate control, walk+cert green by construction; (2) a REGISTRATION instrument scoring every existing room's paint↔proxy disagreement so '98%' becomes a measured number and the ship/kill call per room is instrumented; (3) hybrid path per the premise study (3D structure + paint demoted to non-collidable dressing) as hypothesis D, pure 3D-painterly as fallback C. |
|
DECISION (orchestrator, 2026-07-22 ~22:00, trade-study-informed): PRIMARY = 3D-FIRST (A), with the HYBRID dressing variant (D) measured in the same box session. FALLBACK = C-lite (coherence-derived per-cell walkmask as a safety gate on legacy rooms). B (per-object re-registration) REJECTED for scale — no reliable per-object correspondence on painterly art (braziers are the only findable beacons; the dwing 126px local non-affine case is its native failure mode); its refutation experiment runs only if A/D fails beauty. Why: agreement is only GUARANTEED by construction; the owner declared agreement the sole blocker and authorized discarding beauty-first rooms; the diffusion style pass injects per-object local drift no whole-plate solve can fix; every AAA isometric precedent renders 3D then paints. The deciding session (one box session, A+D together): (1) re-author the crypt as a Synty PolygonDungeonMap/DunGen kit scene via a COMMITTED build script (CANONICAL discipline); (2) Beautify 3 + WOSRelight painterly post (+ optional LOW-strength img2img over the 3D capture); (3) capture through the contract camera → #1680 registration score (expect ~100% by construction) + blind panel vs the crypt 8.3 plate control, judged by WITHIN-PANEL DELTA (the control re-scores ~6.7-7 under the current ruler — absolute 8.3 would false-fail); (4) D variant: composite a NON-COLLIDABLE diffusion dressing layer over the 3D capture → re-score + re-panel + assert no dressing object over open floor; (5) walk gates + player_cert green by construction. Known first hazard: Synty wiring-debt (pink-material risk in the box RP) — material sanity capture is step 0. Panel ≥ control-delta ⇒ A ships standalone; D adds beauty without seam cost ⇒ hybrid; both fail beauty ⇒ B/C refutation experiments before any retreat. |
|
r4 probe data (for the r5 script fix): Synty pillar prefab natural bounds = (0.43, 3.02, 0.43) single renderer. The r3 script instantiates pillars at localScale (4.20, 1.66, 4.20) → world (1.80, 5.00, 0.90) — wide thin SLABS, not columns (x/z inflated asymmetrically; z squashed). Correct treatment: scale so world ≈ (1.2, 4.0, 1.2) — measured-multiplier method proven live (factor = target/measured per axis on the instance's renderer bounds). Walls at 1.5× script height read RIGHT (r4b frame: kit_crypt_r4b.png — brick, tall, capped). r4b pillars still read squat as plain grey boxes → r5 must ALSO assign the stone/brick material to pillars (they are staying default grey) + verify the 4u height actually applies (set, then re-measure, assert ±10%). Frames: kit_crypt_r4.png (overshoot cubes — blind multipliers rejected), kit_crypt_r4b.png (measured set). |
…al + wall height (#83) (#1684) r4-probe fixes (measured live on the GEX44 box, issue #83): - Pillar scaling → measured-multiplier: measure the instance's renderer world bounds, multiply localScale per-axis by target/measured for world (1.2,4.0,1.2), then RE-MEASURE + Debug.Log achieved size, loud warning if any axis off >10%. Fixes the r3 z-half-x asymmetry (world (1.80,5.00,0.90) wide thin slabs → symmetric column). - Pillar material: FixMaterials gains a force flag; pillars shipped a valid-but-grey material FixMaterials skipped (r4b left them default grey) → force stone/brick like walls. - Wall height baked to 1.5x the r3 value (WALL_H 3.6 -> 5.4); r4b frame read right at 1.5x. - r3 lighting / braziers / tomb unchanged.
Kit rounds r5-r6 + FIRST registration score on a kit-built room (2026-07-22 ~22:45)r5 (PR #1684, merged): measured-multiplier pillar scaling landed — in-engine assert logs show all 4 pillars achieve world (1.20, 4.00, 1.20) exactly on target; pillar force-material live (material_fixes 3→7); walls at 5.4u. r6 (PR #1685, auto-merging): r5 eyeball caught pillars rendering as SHORT STUBS despite on-target size → box probe: every pillar at pos.y=-1.98, bounds -1.99..+2.01 — half the column underground. Root cause: PlacePillar's seating formula First registration score of the kit crypt (the spike's core question): NOT the ~100%-by-construction I predicted — and the per-cell decomposition says exactly why (this is the instrument doing its job):
r7 lane dispatched (packet: LEXAR session-notes 2026-07-22/g4-build/dispatch/kit-room-r7-spec.md): impassable-set coverage rule (buttress/plinth for uncovered cells), wall base thickness ≥1.4u, camera-side parapet 0.55u, footprint-driven pillars, open doorways, flat-light Frames: kit_crypt_r5/r6(.png), heatmap + registration JSON at LEXAR session-notes 2026-07-22/g4-build/artifacts/. |
…#1685) * renderer: kit pillar grounding — seat bounds min.y on the floor (r6) PlacePillar's y-term (pivotOffset.y - b1.min.y) double-counts the bounds centre for base-pivot meshes: Synty pillars (pivot at base, centre.y=+2.0 after scaling) landed at pos.y=-1.98 with world bounds -1.99..2.01 — half the column underground, reading as a short stub despite achieved size (1.20, 4.00, 1.20) being exactly on target. Fix: ground on min.y alone (position.y - b1.min.y). Box-verified live: all 4 pillars now minY=0.00 maxY=4.00; capture kit_crypt_20260722T1538Z shows full-height columns. Part of #83 / #84 navigation-truth spike. * renderer: CaptureRoom also emits the 1344x768 contract frame (r6) qa/registration_score.py refuses non-contract sizes (its projection is defined in the 1344x768 frame); the 2560x1600 review render alone could not be scored. One capture op now writes both files.
…core capture (#83) (#1686) Six placement/lighting fixes driven by qa/registration_score.py per-cell attribution of kit_crypt_r6 (58.85% vs the 99% bar; 60 invisible-wall + 19 walk-through cells): 1. IMPASSABLE COVERAGE: every geometry impassable cell left visually open (interior buttress cells (2,1),(3,1),(9,1),(11,1),(12,1)) now gets a deterministic stone mass — buttress if adjacent to a wall_run, else plinth. 2. WALL BASE: walls thickened to >=1.4u and centered on their cell. 3. CAMERA-SIDE PARAPET: cut-away near walls render a low 0.55u parapet. 4. PILLAR FOOTPRINT-DRIVEN SIZE: pillar x/z derived from the authored footprint span so a 1x2 footprint reads covered on both cells. 5. DOORWAYS OPEN: closed door leaf disabled so the doorway reads walkable. 6. FLAT-LIGHT SCORE FRAME: CaptureRoom emits a third 1344x768 contract PNG with room lights off + flat grey ambient (placement, not luminance).
r7 verdict (2026-07-22 ~23:15) — masses landed, the residual is ALBEDO, and the certification design fell out of itr7 (PR #1686, auto-merging) box cycle: +5 buttresses at exactly the predicted uncovered impassable cells, walls 25→50 (≥1.4u bases re-centered + 0.55u camera-side parapets), pillars footprint-driven (1.70×4.00×3.40 filling their 1×2 footprints, in-engine assert), doorways verified bare, and CaptureRoom now emits a flat-light Scores: flat 67.19% (walk-through 19→2 — light pools eliminated) · lit 72.40% (invisible-wall 60→35). Cross-referencing flat∩lit isolates the entire residual to ONE class: dark mass on similar-toned floor — every one of the 35 shared cells is a placed, visible mass scoring 0.33–1.73 against BLOCK_T 1.85. Light pools score 3.3–5.6 (they can never be tuned under threshold in a fire-lit room). Certification design this forces (recorded as the spike's ship-gate story):
r8 lane dispatched (packet: LEXAR g4-build/dispatch/kit-room-r8-spec.md): pale stone floor vs dark masonry masses (shared runtime materials), stone plinths under braziers, taller/darker rubble, dark fallback boxes. Expect flat ≥95% next cycle; iterate the residual list to 99. |
★ 2026-07-23 ~00:30 — REGISTRATION GATE HIT 100.00% (bar 99) — the construction process is provenThe arc from tonight's baseline: paint-first crypt 48.4% → kit r6 58.85% → r7 masses 67-72% → r8d 100.00%. The two instrument rounds that closed it (both measured, both inline on PR #1687):
Verified by eyeball (pixels-in-chat rule): the seg frame is a genuine footprint mask — door gaps open, all 5 buttresses + 4 piers + tomb + braziers + rubble at their authored cells. Not a degenerate pass: the same instrument scored 59.90 on this exact scene one capture-design earlier. The certified chain for 3D-first rooms: geometry JSON → kit assembly (impassable-set coverage rule) → footprint seg gate (must be ≥99, crypt = 100.00) → the lit contract capture IS the plate (provenance: same scene, same camera — no drift possible) → painterly post tunes beauty freely without touching the gate. Remaining for #83 exit: painterly post (Beautify Builtin + WOSRelight) on the lit capture → blind panel within-delta vs the crypt 8.3 control → adopt as the crypt plate → box player build → sandbox walk gate → owner. Evidence: kit_crypt_r8d_score.png + registration JSON at LEXAR g4-build/artifacts/. |
Beauty stage opened (2026-07-23 ~00:00) — Beautify live on the box; verdict: shader post alone won't reach the bar; D-variant nextBox ops battle log (runbook-worthy): the Beautify import chain wedged the editor bridge for ~25 min — THREE stacked blockers, all diagnosed by screenshot: (1) Unity's Script Updating Consent dialog (API updater for Beautify's VRCheck.cs) silently blocks ALL script compilation → the MCP WebSocket client never re-arms after the import's domain reload — clicked '[Yes, just for these files]' via xdotool (⚠ the box root screenshot is 3840x2160 — a wrong scale factor sent my first click into the scene view); (2) the RPG-anim-pack SetupInputLayers.cs nag modal re-spawns every reload; (3) deleting that nag script OUTSIDE Unity desynced the compile graph (CS2001 'source file could not be found' → Assembly-CSharp-Editor fails → every execute_code snippet dies with success:false/null) — recovered by git-restoring the file + focus/Ctrl+R refresh. Bridge now healthy; box autosaved (9484e5924). Beauty pass 1 (kit_crypt_beauty1.png): Next: feed the lit kit base through the Gemini restyle (paint_room's styled pass with the crypt recipe + structure locks) → within-panel delta vs the 8.3 control (two-anchor rule) → if in-band, adopt as the crypt plate + wire the seg gate into the room pipeline. PR #1687 (r8+r8b/c/d incl. the seg gate) auto-merge armed, CI cycling. |
D-arm round 1 (2026-07-23 ~00:20): direct Gemini restyle of the 3D render = HONEST NEGATIVE (total recompose); depth-anchored D2 launchedD1 (kit_crypt_beauty2_contract → The fix is the pipeline we already trust: the kit scene now emits its own DEPTH map through the contract camera (LinDepthRemap replacement-shader pass, box-side; kit_crypt_depth.png — real 3D profiles for every pier/buttress/brazier/tomb, far richer than greybox depth). D2 = the FULL proven chain on that depth — 3 flux depth-CN draws → edge-recall selection → Gemini structure-lock → the solve-based err_cells hard gate (--boxes armed) — i.e. the exact recipe that made the 8.3 control, conditioned by the 3D scene instead of a greybox. Running now (~47 CU). If D2 lands in-band on the two-anchor panel AND passes err_cells, the full story closes: geometry → kit scene (seg gate 100%) → 3D depth → proven paint chain (err_cells) → plate whose collision agreement is certified at both ends. |
Two-anchor panel on the gate-compatible candidates (2026-07-23 ~01:00) — both OUT of band; the decision matrix is now fully measuredBlind 5-scorer control-anchored panels (versioned ruler, A/B alternated; control = shipped crypt plate, reproduced at 8):
The spike's trade, in numbers:
MAI 2.5 Edit is currently infra-down on Scenario (two jobs stuck warming-up 15-20 min; third attempt polling with a 1200s window). If it lands and reproduces its bake-off behavior on the kit-depth flux base: seg gate 100% + structure-held style pass + in-band panel = the full 3D-first chain closes end-to-end. If it stays down: the beauty half parks pending the retry — the certification machinery (impassable-coverage builder + seg gate + 3D-depth conditioning, PRs #1682-#1687) is the durable yield either way. Panel evidence: workflow wf_72a16275 notes on this thread's artifacts dir. |
…acement gate (#83) (#1687) * renderer: build_room_kit r8 — albedo separation for the flat-light placement gate (#83) r7 flat-light score 67.19% (lit 72.40%) vs the 99 bar; all 35 shared invisible-wall residuals were one class — a correctly-placed dark stone mass on a similar-toned floor, scoring 0.33-1.73 vs BLOCK_T=1.85. Make the flat capture albedo-separated so it measures PLACEMENT: 1. Floor: one shared KitFloor_PaleStone (pale warm-grey, cloned from the tile's own material to keep the kit texture) on every floor tile. 2. Mass: one shared KitMass_DarkMasonry forced onto walls/parapets/ buttresses/plinths/pillars/rubble. Tomb footprint (7-9,5-6) was the worst residual cluster (cells 0.13-0.84, dead-on-floor) -> darkened with KitProp_Stone (focal-prop mid-stone). 3. Brazier bases: dark stone plinth 1.3x0.45x1.3 under each brazier. 4. Rubble raised to 0.85u + dark masonry. 5. Wooden fallback boxes get KitProp_DarkWood (barrel (10,7) scored 0.33). 6. Lighting rig / braziers / capture logic / pillar sizing / buttress pass unchanged from r7. Runtime-only shared materials, reset per build, never written to the AssetDatabase; deterministic, no RNG. * renderer: score capture at ambient 1.0 — render surfaces AT albedo (r8b) Flat ambient 0.6 crushed the score frame into a narrow dim band; the coverage stat (calibrated on bright painterly plates) under-read every mass — measured flat score DROPPED 67.19→59.90 after the r8 separation pass. At 1.0 flat, surfaces render at their albedo and the authored 0.72-vs-0.34 separation reaches the scorer. * renderer: segmentation score frame — floor white, mass black, unlit (r8c) Albedo tints cannot separate floor from mass through the kit textures (everything converges to warm brown; flat score 67.19→59.90 across r7→r8 measured). The gate question is 'which floor quads are covered by mass through the contract camera' — render it as exactly that: floor renderers flat near-white, all other renderers flat near-black, unlit, lights off. Materials restored in finally; beauty stays in the lit frame. * renderer: seg frame flattens masses to footprint slabs (r8d) Tall seg-black masses screen-occlude the floor quads of walkable cells behind them (measured: 50 false walk-throughs in r8c). The gate is a FOOTPRINT question — flatten Walls/Props/Impassable to 0.02x-height floor slabs for the seg render (y-scale+y-pos scaling preserves floor seating) and hide door-frame renderers so lintels/arches don't drop onto their open door cells. All transforms restored in finally.
Night close (2026-07-23 ~02:15) — beauty half PARKED on a probe-verified provider outage; registration half SHIPPEDMAI 2.5 Edit is down on Scenario: 3 jobs (job_kZJ…/job_7xq…/job_YsV…) across ~45 min all died stuck at What shipped tonight (all merged to main, #1682→#1687): the kit-room construction pipeline + the registration certification chain — crypt registration 48.4% → 100.00% (0/192 disagreements), with the full per-round evidence and honest negatives (Gemini recompose ×2, panel Δ−5/−6 on the raw renders) on this thread. The remaining rooms can be kit-rebuilt to 100% registration through the same chain as soon as the styler question resolves — or with interim flux-base plates if beauty must wait. Owner's framing answered where it stands: building a navigable room at a fundamental level is now a measured, repeatable, gated process. Making it beautiful without breaking it is one working edit-model away, with in-scene material dressing as the fallback. |
★★ OWNER-STEERED BAKE-OFF ROUND 2 (2026-07-23 ~02:45) — the models were never broken; MY payloads were. Two finalists IN BAND.Owner's diagnosis confirmed: MAI never had an outage — my freehand call carried
Compare last night's Δ −5/−6 for everything gate-compatible. The chain now closes end-to-end for the first time: geometry → kit scene (seg gate 100.00%) → 3D depth → flux base (0.9752) → schema-correct structure-holding edit → panel IN BAND. Honest open gate: the forensic registration instrument reads the styled finalists at 45/39% — but it reads ALL painterly images this way (documented light-pool + dark-mass classes; it scored the shipped plate 48.4% and even the raw kit render 58-72%). It cannot certify painterly layers; that's what the seg gate + provenance are for. The styled layer's formal per-object verification is the ONE remaining check: next round re-runs the winner with all four braziers lit so the 4-point beacon err_cells solve is valid (last night's solves were fitting on 2 fires = meaningless). Also corrected for the record: MAI's corrected-payload job is still processing (d4) — its result completes the ranking. Evidence: d5/ artifacts + submitted bodies + heatmaps at LEXAR g4-build/artifacts/d5/. ~103 CU this round. |
d6 fire round (2026-07-23 ~03:00): GPT Image 2 fire plate = the run's strongest candidate; beacon instrument declared unfit for painterly platesWith the all-four-braziers-lit clause, GPT Image 2 produced fires at exactly the authored beacon cells with structure held (eyeball) — raw err_cells 0.79 → warp 0.41, BUT the solve's internals are invalid on this class: n_blobs=27 (painterly glow highlights everywhere), fitted_ortho 20.8 vs stamped 11.8 = the correspondence is garbage, so neither 0.79 nor 0.41 is meaningful. Gemini-3-Pro-fire read 2.88 for the same reason. Verdict recorded: the beacon blob-solve joins the coverage stat as unfit for rich painterly plates. The styled-layer formal gate must be a PER-OBJECT VQA check (candidate vs base object positions — the adjudicated-VQA machinery from the coherence instruments): tomorrow's build, charter noted here. Candidate plate of record: d6/gpt_image_2_fire.png (panel-in-band family Δ−1.0, fires at beacons, structure eyeball-held over the provenance-perfect base). Runner-up: d5 Gemini 3 Pro (best recall 0.886, 2K native). Also for the record per the owner's steer: Scenario on-platform workflows are API-accessible (workflow_create/run via MCP) and the account currently has ZERO defined — candidate: codify base→edit→verify as an on-platform workflow. Flux 2 family confirmed present (Max/Pro/Flex/Dev + klein LoRA-trainable bases; flux.2-dev-lora model type exists) — our painterly LoRAs are all Flux-1-era; retraining on Flux 2 is a real lever (training spend = owner gate). ~198 CU total tonight. |
|
MAI correction (2026-07-23 ~03:15): the corrected schema-conformant job (job_7YUbMkC6d3Z6K2xbYnnHM9iw — verified clean payload, prompt 2500/4096) ALSO hung at warming-up 15+ min with no error field, statusHistory stuck at queued. So the honest two-cause verdict: (1) my three earlier payloads WERE malformed (invalid numSamples/resolution — owner's diagnosis correct, schema-first rule now standing), AND (2) MAI's Scenario worker pool is genuinely not picking up jobs right now — every other model completed in <3 min tonight. MAI stays parked for a later retry; GPT Image 2 + Gemini 3 Pro carry the styled-plate lane (both in-band, both structure-holding). Note: the stuck jobs show billing.cuCost 12 each — verify whether warming-up-timeouts actually charge (≤48 CU exposure if so). |
d7 round (2026-07-23 ~03:45) — style-mix + tamed Seedream + Flux2-LoRA probe, panelledOwner-suggested experiments, all run inline schema-first:
Notes for the record: MAI normal (model_microsoft-mai-image-2-5) is txt2img-only — cannot edit; the edit variant stays parked on its worker-pool issue. Two memories written from the owner's feedback: schema-first model calls + check-it-yourself-first (diagnosis and creative iteration are Fable-inline). Queue: (1) per-object VQA gate for the styled layer → formally certify the d6 champion; (2) adoption chain (manifest swap + registry allowlist proposal OWNER GATE + box build + sandbox walk gate + owner install); (3) fresh Flux 2 LoRA train (klein/dev base, curated PoE2 set) as the beauty-ceiling lever; (4) remaining 6 rooms through geometry→kit→depth→edit. ~270 CU total tonight. |
★★★★ ADOPTION EXECUTED (2026-07-23 ~04:45) — KIT-CRYPT v1 is the first fully-certified 3D-first plate; PR #1688 armed; box build runningWinner: Gemini 3.0 Pro, critique-targeted round (d8_gemini_a) — blind panel 7 vs control 8 (Δ−1.0, in-band), phase-correlation ALIGNED dx=0,dy=0 against the depth-proven base, knight effigy + carved knotwork + all four fires at authored brazier cells. Runner-up standings: d6 GPT-2 champion warped-to-aligned 6.5 (Δ−1.5); three others Δ−2. The styled-layer gate that closed the last link (built tonight, numpy-only): Shipped in PR #1688: the plate (geometric void composite via the seg-frame mask), manifest swap (ortho pin preserved, boxes unchanged, +4 runtime fire effects at brazier cells), the alignment gate, seg evidence. ⚠ Owner gate flagged: model_google-gemini-pro-image-editing → registry allowlist. In flight: box player build (kit scene root removed + scene saved first — the QA construction must not ship embedded). On build: sandbox walk gate → frames → owner install. ~410 CU tonight. |
… (#1688) The certified chain: geometry -> kit scene (seg gate 100.00%, 0/192) -> 3D depth -> flux base (recall 0.9752) -> Gemini 3.0 Pro structure-holding edit -> phase-correlation ALIGNED dx=0 vs base -> void composite. Blind panel 7 vs control 8 (in-band). Collision truth unchanged. + qa/styled_align_check.py (styled-layer alignment gate v0) + 4 runtime fire effects at the authored brazier cells.
★★★★★ SHIPPED TO THE OWNER (2026-07-23 ~05:40) — the certified kit-crypt is LIVE on the owner's machineOwner frame verified (their world: To Tavern/To Throne Hall labels, Aldric, no monsters — the QA-rig frame was caught and discarded by the serving-identity check first): the painterly certified plate renders in their installed player, with the walk-behind silhouette composing correctly against it. Install: backup at ~/worldos-session-notes/kitcrypt-2026-07-23/backup-preinstall-20260722T193315Z, xattr+codesign, double-kickstart. No reseed needed — the plate adopts by location id over the UNCHANGED walkslice grid. Two more real defects were caught and fixed by pixels-before-credit during the ship chain:
Sandbox verification on the shipped binary: walk gate — 36/36 open cells, 26/26 impassable rejections, 2/2 doors, 36/36 paths (the only 2 'fails' = goblin-occupied cells where the engine correctly refuses to stack). Fire effects spawn at all 4 authored brazier cells (Hovl legacy-shader warning noted — flames render dim; polish item). The owner's 6-week problem, closed end-to-end in one night: geometry → kit 3D scene (seg gate 100.00%) → 3D depth → flux base (0.9752) → Gemini 3 Pro edit (panel 7 vs 8, in-band) → phase-corr aligned → adopted → box build → walk green → installed. NEXT: replicate the chain on the remaining rooms starting with the tavern — the 'stone cold every time' proof. PR #1688 auto-merging. |
★★ TAVERN REPLICATED (2026-07-23 ~07:00) — second room through the certified chain in ~80 minutes, PR #1689 armed
New load-bearing recipe clause discovered: FURNITURE-IDENTITY grounding — naming what each kit mass IS ('the long counter-shaped mass is the BAR — worn oak, brass rail; the square boxes are round oak TABLES…') moved the panel a full point in one cycle while alignment stayed dx=0. Recorded in the manifest provenance; belongs in the recipe schema next. The winner: timber-frame + stone walls, roaring hearth, iron-hooped barrel stacks, carved bar with brass rail, tables with mugs — every mass on its authored footprint. Box build packaging BOTH kit plates running → tavern walk gate → owner install #2. The chain is now demonstrably a PROCESS, not an artifact: crypt ~8 hours including all instrument-building; tavern ~80 minutes using it. |
Tavern ship-gate: WITHHELD on an honest eyeball catch (2026-07-23 ~07:45) — grey occluder tops over the new plate's furnitureTavern walk gate on the new build: FULL GREEN — 30/30 reachable, 21/21 impassable, 2/2 doors, 30/30 paths, zero fails. Plate renders beautifully live. BUT the eyeball caught grey proxy slabs standing above the painted tables/bar/candelabra bases: the tavern_v2 boxes sidecar's occlusion volumes (built chunky for the OLD plate's depth cues) are TALLER than the new painting's furniture, and their exposed tops render visible. Ruled out: truth overlay (sandbox doesn't set it), stale saved objects (37 purged, rebuilt, still present — so runtime-built). Owner install of the tavern build WITHHELD (pixels-before-credit). The owner's current install is unaffected — its old tavern plate matches the old proxy heights; their crypt remains the certified kit plate. Diagnosis queue (next session, fresh eyes): (1) why are the proxies VISIBLE at all — expected ColorMask-0/renderer-off; check the occluder material path in CombatSurfaceClient for a stripped-shader fallback (the #1674 class — the always-included list carries only OccluderDepth+ActorSilhouette); (2) if visibility is by-design-when-material-fails, the fix is baking the occluder shader into EnsureAlwaysIncludedShaders; (3) independently, regenerate the tavern sidecar volumes from the KIT scene's real mass heights (the kit knows every mass's true height — the sidecar should inherit it). Scene hygiene fix that DID land: 37 stale saved runtime objects (Occluder_/Actor_/HP_*) purged from the canonical scene + the PaintedBackdrop-disabled trap documented. Night ledger: crypt SHIPPED to owner (certified end-to-end) · tavern adopted on main (PR #1689) + walk-green + one named visual defect blocking install · process replication proven (100.00% seg first-try, ~80 min/room) · ~520 CU. |
Summary
WKWebViewinstead of replacing viewer state logic..appbuild/run script, Codex Run action, local signing checklist, and safer double-click launcher preflights.Refs #82
Architecture Notes
scripts/play_party.shpath.play-stateandqa/statesnapshots only.Validation
bash -n scripts/launch_common.sh scripts/play.sh scripts/play_party.sh clawdnd-play.command script/build_and_run.shpython3 -m py_compile viewer/server.pyswift build --package-path macos/ClawDnDApp./script/build_and_run.sh --verifycodesign --verify --deep --strict dist/ClawDnD.appplutil -lint dist/ClawDnD.app/Contents/Info.plist/state,/dashboard,/monitor.jsonagainst an existing QA state dirgit diff --checkNot Run
Summary by CodeRabbit
New Features
Chores