Add WitcherScriptMerger.Headless: a Linux-capable CLI/MCP-only host - #8
Closed
TheValiantOne wants to merge 34 commits into
Closed
Add WitcherScriptMerger.Headless: a Linux-capable CLI/MCP-only host#8TheValiantOne wants to merge 34 commits into
TheValiantOne wants to merge 34 commits into
Conversation
Migrates from .NET Framework 4.5 (2015-era csproj) to a modern SDK-style project with implicit file globbing, System.Configuration.ConfigurationManager for App.config compatibility, and System.IO.Hashing pulled in ahead of replacing the hand-ported xxHash implementation. Removes unused empty Settings.settings scaffolding and an obsolete CAS attribute the runtime no longer honors. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0139CvhVjVPBuSNHFcq9TF27
Swaps the 2015 hand-ported xxHash implementation (Tools/xxHash.cs) for the officially maintained System.IO.Hashing.XxHash32, streamed the same way the original was. Verified byte-for-byte hash compatibility across empty/boundary/large inputs and against a real mod file already tracked in a live MergeInventory.xml (modBetterIcons2025_NextGen's baseEffect.ws, recorded hash 6A33E604, matches exactly) - existing merge inventories stay valid, no forced re-merges. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0139CvhVjVPBuSNHFcq9TF27
Previous commit only captured the file rename (xxHash.cs -> Hasher.cs); the git add for the content change silently no-opped due to a bad pathspec on the already-moved old filename, so the real replacement never got staged. This is that content. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0139CvhVjVPBuSNHFcq9TF27
…otes Ships operational guidance and coding standards openly with the repo, while keeping session-scoped handoff notes (HANDOFF*.md) and Claude Code's own runtime-state files out of source control for machine/session-privacy reasons. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Vanilla .ws files are UTF-16LE; mod authors' files are often plain UTF-8/ASCII with no BOM. KDiff3 has no per-input encoding flag, and the mismatch made it treat an entire file as unmatchable and fall back to manual GUI resolution instead of auto-solving - confirmed against a real conflict (baseEffect.ws). Normalize non-UTF-16LE inputs up to UTF-16LE+BOM (matching vanilla, never down to UTF-8) before invoking KDiff3. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…tifact Re-verified via .NET's Process.Start (the actual code path this app uses, not a shell) while researching CLI mode: damageManagerProcessor.ws auto-solves cleanly either way. The prior claim that it still needed manual resolution came from testing through Git Bash/MSYS2, not the real invocation method. Also documents the actual window-title signal that distinguishes a genuine unresolved conflict from KDiff3's normal transient startup window, found while building a synthetic guaranteed conflict to verify this properly. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Preparation for a CLI mode: Paths.GameDirectory read MainForm's textbox directly (Program.MainForm.GameDirectorySetting), which would null-ref with no MainForm constructed; it now reads the same underlying Program.Settings value the textbox was only ever mirroring. Every domain-code call into Program.MainForm.ShowMessage/ShowError/ ShowModal (FileMerger, ModFileIndex, CustomLoadOrder, KDiff3, WccLite, QuickBms, AppSettings, Program's own launch-failure path) now goes through a new Program.Notifier (IMergeNotifier) instead. MainForm implements the interface directly - its existing methods already match the shape exactly - so the GUI path is unaffected. A new HeadlessMergeNotifier backs it for non-interactive runs: writes to the console and returns a fixed, non-destructive default for every prompt (don't overwrite, don't use a conflicting merge name, don't retry) so a batch run can never hang waiting on a dialog nobody's watching. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
KDiff3 has no fail-fast mode - its own docs (doc/dothemerge.html) say a merge window opens whenever manual interaction is needed, even in its own batch/automation mode. RunHeadless launches it normally and detects a stuck merge itself instead: KDiff3 always briefly shows a plain "Conflicts" window on startup regardless of outcome (not a signal, this was verified empirically and had been mistaken for one), but only a genuine unresolved conflict leaves open a second window titled "... - KDiff3", the actual comparison editor. If that persists past a short grace period the process is killed and the merge reported as skipped, with a generous backstop timeout as a second line of defense. -o always targets a scratch path, copied to the real output only after a confirmed clean exit, so a killed process can never leave a partial file where the game would load it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
`WitcherScriptMerger.exe merge [--order-file <path>]` scans for conflicts and merges every auto-solvable one without opening any window, using the Program.Notifier/KDiff3.RunHeadless plumbing from the last few commits. No arguments still launches the GUI exactly as before. FileMerger.MergeConflictsHeadless is a new, separate orchestration path driven by FileIndex/ModFileIndex.Conflicts' plain ModFile/FileHash data - that already carries everything needed (relative path, category, each mod's file and hash), so no TreeNode is built for this path at all. Per- file mod order defaults to the same LoadOrderComparer ordering ConflictTree already sorts by, overridable per relative path via a JSON --order-file for cases that need it. Bundle-packed conflicts aren't included yet (checkBundles: false) - a separate pass once this flat-file path is proven out. Console output needs AttachConsole (P/Invoke) since the app is OutputType=WinExe; it's called as early as possible in Program's static init, before AppSettings even runs, so a startup failure is visible in the invoking terminal instead of lost to an unattached console. Verified end-to-end against real conflicting files (encoding mismatch, auto-solves correctly, matches HANDOFF.md's pending modToxicityLevelFix scenario) and a synthetic guaranteed conflict (correctly detected, process killed cleanly, reported as skipped) in a scratch game/mods tree - never against the live install. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
MergeConflictsHeadless now handles Categories.BundleText alongside flat files, reusing FileMerger's existing GetUnpackedFiles (QuickBMS unpack) and PackNewBundle (wcc_lite repack) exactly as the interactive path does - both were already TreeNode-free, so nothing there needed to change. Bundle merges batch into one repack at the end, matching MergeByTreeNodesAsync's existing behavior, and roll back to "skipped" if the repack itself fails after content merged fine. CLI now passes CheckBundleContents through to the conflict scan instead of hardcoding it off. Verified the scan + orchestration run cleanly with bundle checking enabled (no regression against the flat-file path, confirmed over multiple runs); didn't have a real bundle-vs-bundle conflict on hand to round-trip through actual QuickBMS/wcc_lite content, so that specific path is code-reviewed and mirrors the proven flat-file orchestration but isn't end-to-end verified the way the flat-file path is - noted in CLAUDE.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Covers the merge command and --order-file, the IMergeNotifier split and why it exists, RunHeadless's window-persistence detection, and an honest verification status split: flat-file path is end-to-end verified, bundle path is code-reviewed and reuses proven building blocks but hasn't been round-tripped through a real bundle conflict. Also updates the Architecture section's now-outdated claim that domain code reaches Program.MainForm directly, and marks CLI mode/whitespace done in the open-goals summary. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
KDiff3's pop-up window can't be hidden without breaking its ability to auto-solve. Tested five techniques against a real auto-solve case and a guaranteed-conflict case: WindowStyle=Hidden and =Minimized are both silently ignored by KDiff3/Qt (window shows regardless, but nothing breaks); ShowWindow(SW_HIDE), SetWindowPos moved off-screen, and launching on a separate non-interactive Windows desktop all three genuinely succeed at making the window invisible - and all three reliably make KDiff3 hang forever at its "Conflicts" splash instead of ever auto-solving, confirmed against a clean control that auto-solves normally every time when left untouched. RunHeadless now accepts the window appearing and instead attempts to restore focus to whatever had it beforehand once KDiff3's window is confirmed gone (waits on the kill path since Kill() is async). Plain SetForegroundWindow was empirically denied every time due to Windows' foreground-lock rules, so this upgrades to the standard AttachThreadInput workaround - but that was *also* denied in every test run in this session's sandboxed automation environment, so CLAUDE.md documents the restore as an unverified best-effort mitigation, not a proven fix. Also documents an accidental finding: polling faster than the current ~250ms interval hangs KDiff3 even with zero window manipulation, likely GetWindowText's cross- process SendMessage starving its message loop - load-bearing, noted so nobody "optimizes" it into a hang later. Also adds `WitcherScriptMerger.exe mcp`, a stdio MCP server (official ModelContextProtocol NuGet package) exposing four tools - scan_ conflicts, merge_conflicts, get_status, list_merges - so an MCP client (e.g. Claude Code) can inspect conflicts and drive merges directly instead of only through one-shot CLI invocations. Extracts the shared scan-then-merge sequence into Cli/MergeOperations.cs, used by both the `merge` CLI verb and the new MCP tools. Logging is routed to stderr (stdout is reserved for MCP protocol frames), and AttachConsole is skipped for the mcp verb since an MCP client redirects stdio itself. The two MCP tools that touch the shared Program.Inventory are wrapped in a lock, since an MCP client can issue concurrent tool calls and an interleaved Load/Save would corrupt MergeInventory.xml. Verified: the focus-restore and polling-rate findings against real KDiff3 runs in a scratch game/mods tree; the MCP server via a hand- rolled stdio JSON-RPC client exercising initialize, tools/list, and all four tools (including a merge_conflicts call that hit the real RunHeadless kill path) against the same scratch tree. Never run against the live install. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Establishes indent_style=tab (4-wide) for .cs files going forward, applied here via `dotnet format whitespace` across the whole solution. Purely mechanical - confirmed with git diff --ignore-all-space that nothing but whitespace changed, plus 6 files (added this session, before this convention existed) picking up the UTF-8 BOM the rest of the codebase already uses. Build verified green after. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Now that this fork is a real public GitHub repo (TheValiantOne's fork of AnotherSymbiote/WitcherScriptMerger, replacing the earlier local- only setup), the old "direct commits to master, no PR workflow" SOP no longer fits. main is now branch-protected on GitHub: no direct pushes, PRs require 2 approving reviews (admin bypass left enabled so a single maintainer isn't locked out before other reviewers are active). CONTRIBUTING.md documents feature/fix/chore branch-per-change, what a PR description needs to cover given there's still no test suite, and updates the code-style section for the tabs switch in the prior commit. Expands the AI-assisted development section into explicit rules for outside contributors, not just the maintainer's own disclosed use: disclosure in the PR description, the contributor owning and being able to explain whatever an agent produced, the verification bar not softening for agent-assisted changes (this codebase's hash-format/ encoding/KDiff3-invocation constraints are exactly the kind of thing an agent has no way to know unless CLAUDE.md tells it, and no way to confirm without actually running it), scrubbing machine-specific paths picked up during an agent session before submitting, and license awareness for agent-suggested code. Extends .gitignore with a scoped entry for Aider's local session state, matching the existing .claude/ runtime-state block's reasoning, plus a note inviting the same pattern for other tools' local state. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
PromptToPrioritizeMergedMod called MessageBox.Show directly instead of Program.Notifier.ShowMessage, unlike every other domain call site. Its sole caller today is invoked only from MainForm.cs, so it was harmless in practice, but it was a landmine for any future headless (CLI/MCP) load-order validation path, which would otherwise hit an unmediated WinForms MessageBox.Show with no message pump watching it. IMergeNotifier.ShowMessage gained a trailing optional MessageBoxDefaultButton parameter (default Button1, matching MessageBox.Show's own default) so the prompt's Button2 (No) default survives the move - dropping it silently would have flipped the default action from "leave load order alone" to "rewrite mods.settings". MainForm.ShowMessage forwards it to the 6-arg MessageBox.Show overload; HeadlessMergeNotifier ignores it. The MessageBoxManager relabeling of the Cancel button to "Ne&ver" is removed rather than preserved: it depends on a SetWindowsHookEx hook registered on the calling thread, but Program.Notifier.ShowMessage (MainForm.ShowMessage) marshals the actual MessageBox.Show call onto the UI thread via Invoke when called off-thread - as this call always is, via MainForm's Task.Run - so the hook would never see the dialog's window messages once routed through the notifier. Kept as dead code it would look functional without being so. The Cancel button now reads "Cancel" instead of "Never"; the DialogResult value and its handling in ValidateAndFix are unchanged. Also guarded ValidateAndFix's Cancel branch on Program.Notifier.IsInteractive: HeadlessMergeNotifier's fixed non-destructive default for YesNoCancel is Cancel, which previously mapped to "Never" here and would have silently persisted ValidateCustomLoadOrder=false to App.config on any future headless run that reaches this code path - exactly the landmine this change exists to defuse. Verified: Program.Notifier is reassigned to MainForm in Program.cs before Application.Run, and the only path that reaches PromptToPrioritizeMergedMod (MainForm_Shown -> RefreshMergeInventory -> LoadOrderValidator.ValidateAndFix) runs after Shown, never from MainForm's constructor - so this change doesn't introduce a window where the prompt silently goes to a HeadlessMergeNotifier instead of the GUI. No GUI automation harness is available in this environment; verified by dotnet build (no new warnings), dotnet format whitespace --verify-no-changes, and code inspection confirming button set, icon, message text, and DialogResult handling are unchanged from the original MessageBox.Show call. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GXAuGMLB44T5Zv5o5ZzKah
Runs dotnet build and dotnet format whitespace --verify-no-changes on every PR targeting main, per the recommendation in HANDOFF.md. Also normalizes checkout line endings to CRLF (git config --global core.autocrlf true) so the format check sees the same line endings on the windows-latest runner as a local Windows dev machine, since .cs files are committed as LF with no .gitattributes to normalize them. Lightly updates CONTRIBUTING.md's pre-PR verification note to mention this now also runs automatically in CI. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GXAuGMLB44T5Zv5o5ZzKah
Researches whether WolvenKit.Modkit/WolvenKit-7 could replace QuickBMS + wcc_lite for .bundle unpack/repack. Finding: WolvenKit.Modkit targets Cyberpunk 2077 (wrong engine/format entirely); WolvenKit-7 (the actual Witcher 3 tool) has an unimplemented metadata.store writer (confirmed via its own GitHub issue #33 and a literal TODO stub in current source) and its own pack pipeline still shells out to the same closed-source wcc.exe WSM already depends on. Recommends staying parked on QuickBMS/wcc_lite (option c) rather than relicensing WSM or adding a new external dependency for no net capability gain. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GXAuGMLB44T5Zv5o5ZzKah
Design document for a future Vortex (Nexus Mods) extension that would drive WSM's existing CLI (`merge`) and MCP (`scan_conflicts`, `merge_conflicts`, `get_status`, `list_merges`) interfaces from Vortex's TypeScript/Node extension runtime, instead of only through direct invocation. Covers tech stack, install/setup, invocation model (CLI-first recommended, MCP as a v2 enhancement), data model mapping between Vortex's mod/load-order state and WSM's mods directory/ mods.settings, proposed UX, and open questions for the repo owner. No code changes - this unit is explicitly design-only per this batch's plan; TypeScript/Vortex implementation is deferred to a later batch. Grounded directly against this repo's CLAUDE.md and against Vortex's actual, current game-witcher3 extension source (fetched via `gh api`) to document real prior art - including its GUI-only tool invocation, its existing WitcherScriptMerger.exe.config rewriting, and its MergeInventory.xml/Collection-import behavior - rather than assuming or inventing capabilities on either side. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GXAuGMLB44T5Zv5o5ZzKah
- LoadOrderValidator.cs: simplify back to unqualified MessageBoxButtons/ MessageBoxIcon/MessageBoxDefaultButton now that the fully-qualified form (matched literally to the task's example) reads as inconsistent next to the same file's own unqualified DialogResult usage and the rest of the codebase's convention wherever `using System.Windows.Forms;` is already present. - LoadOrderValidator.cs: expand the comment on the dropped "Ne&ver" relabel. Review correctly pointed out the old MessageBoxManager hook genuinely worked before this change (MessageBox.Show ran directly on the same background thread Register() hooked, no Invoke involved) - this is a real, disclosed regression in how the Cancel button reads, not a no-op cleanup, and the comment now says so plainly along with why it can't be preserved through Program.Notifier without extending IMergeNotifier with custom button-text support (out of scope here). - HeadlessMergeNotifier.cs: comment on why defaultButton is accepted but not consulted when choosing the headless DialogResult, so it doesn't read as an oversight to a future caller relying on it. Not addressed here, flagged for other units instead: HeadlessMergeNotifier .Write already routes MessageBoxIcon.None messages to stdout, which is a pre-existing MCP stdout-hygiene risk unrelated to this change (belongs to the MCP-hardening unit); MainForm.cs's PromptToDeleteForChangedHash has an analogous still-"Ne&ver"-labeled prompt that now reads inconsistently with this one, but that method is GUI-layer code outside LoadOrderValidator.cs's scope. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GXAuGMLB44T5Zv5o5ZzKah
Move all WinForms-free domain code (FileIndex, Inventory, LoadOrder, Tools/Hasher+QuickBms+WccLite, Paths, AppSettings, Cli, Mcp) into a new net10.0 WitcherScriptMerger.Core class library, so it can eventually run on Linux. The host project keeps Forms/Controls, KDiff3's Win32 P/Invoke, and Program.cs, referencing Core via ProjectReference. Key design changes required to make the split compile, beyond plain file moves - full rationale in the PR description: - AppState (Core): Notifier/Settings/LoadOrder/Inventory/MergeEngine used to be static fields on the host's Program class; moved to a new Core class since Core code needs them and can never reference the host assembly. Program.cs re-exposes them as pass-through properties so host call sites didn't need to change. - IMergeNotifier neutralized: MessageBoxButtons/MessageBoxIcon/DialogResult replaced with NotifyButtons/NotifyIcon/NotifyResult; MainForm translates to/from the real WinForms types. ShowModal(Form) dropped from the interface entirely - every call site was GUI-only, so the host calls MainForm.ShowModal directly instead. - IMergeEngine (Core) + KDiff3MergeEngine (host): scaffolding so FileMerger can move to Core while KDiff3.cs's Win32 P/Invoke stays host-only this wave. - FileMerger split: Core keeps headless + interactive merge orchestration (now driven by plain InteractiveMergeRequest data, not TreeNode); the host's new InteractiveMergeRunner extracts that data from TreeNodes, owns the BackgroundWorker, and shows report forms/plays sounds via OnMergeReport/OnPackReport callbacks. - Extensions.cs split: pure string helpers moved to Core's StringExtensions.cs; TreeNode/TreeView WinForms helpers stayed host-side. - LoadOrderValidator.PromptToPrioritizeMergedMod routed through IMergeNotifier instead of a direct MessageBox.Show call (was flagged as needing this fix to move into Core at all). dotnet build WitcherScriptMerger.sln succeeds with the same 5 pre-existing warnings (all host-side CA1823, unrelated to this change) and zero warnings/errors from WitcherScriptMerger.Core. dotnet format whitespace --verify-no-changes passes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GXAuGMLB44T5Zv5o5ZzKah
- Fix a real regression: Program no longer has an explicit static
constructor after Notifier/Settings/LoadOrder/Inventory became
pass-through properties to AppState, so it picked up beforefieldinit -
under which the CLR is free to defer _consoleAttached's field
initializer (MaybeAttachConsole()) past Main() entirely, since nothing
in Main() touches a field of Program anymore. Confirmed empirically
with an isolated repro (mirrors the exact shape: side-effecting field
initializer + pass-through property to another type + no static ctor)
- without an explicit static Program() {}, the initializer's side
effect never ran before Main() read the forwarded property; with it,
ordering was correct every time. This likely explains an unexplained
empty-output CLI invocation earlier in this same session.
- Rename NotifyIcon -> DialogIcon: the original name, declared in the
root WitcherScriptMerger namespace that every host file already sees
via nested-namespace lookup, would have silently shadowed
System.Windows.Forms.NotifyIcon (the tray-icon class) for any
unqualified reference in host code.
- Add IMergeNotifier.ShowMessage(..., defaultResult) so
LoadOrderValidator's "Custom Load Order Problem" prompt can still
request "No" as the pre-focused button, matching the direct
MessageBox.Show(..., MessageBoxDefaultButton.Button2) call it replaced.
This was previously mischaracterized as a purely cosmetic loss; it
wasn't - an accidental Enter/Space keypress could have silently
rewritten the user's real mods.settings file.
- Remove FileMerger.InteractiveMergeRequest.OrderedModNames: redundant
with OrderedSources[i].Name at its only read site (the pre-loop
ConfirmRemainingConflict gate, which always runs before any element of
OrderedSources is touched).
- Remove IMergeNotifier.IsInteractive: confirmed dead code (zero read
call sites) both before and after the Core split.
- Make Categories.* fields readonly: closes off silent breakage of the
reference-equality checks used throughout the codebase
(`category == Categories.Script`) now that they're public (required
for cross-assembly access) rather than merely assembly-internal.
- Add code comments acknowledging two accepted, lower-severity
tradeoffs raised in review: QuickBms/WccLite's ExePath fields staying
publicly mutable (matches the pre-existing, legitimate
KDiff3.ExePath-via-DependencyForm mutation pattern) and
Paths.ValidateDependencyPaths() reading AppState.MergeEngine as a
global rather than via DI (matches the pre-existing
AppState.Notifier/Settings pattern this PR extends, not a new
anti-pattern).
Re-verified after these fixes: dotnet build clean (same 5 pre-existing
warnings), dotnet format --verify-no-changes passes, CLI merge and MCP
tools/list + merge_conflicts round-trip both still succeed against the
scratch tree.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GXAuGMLB44T5Zv5o5ZzKah
1. LoadOrderValidator's YesNoCancel "Custom Load Order Problem" prompt: its Cancel button permanently disables ValidateCustomLoadOrder, but after the Core split it just reads "Cancel" with nothing marking it destructive (the old MessageBoxManager "Ne&ver" relabeling has no IMergeNotifier equivalent, and was likely already silently broken pre-split regardless - AppDomain.GetCurrentThreadId() doesn't reliably return the real Win32 thread ID its SetWindowsHookEx call needs). Spelled the Yes/No/Cancel semantics out explicitly in the message body instead, where it doesn't depend on any button-caption mechanism working. 2. HeadlessMergeNotifier now honors ShowMessage's defaultResult instead of always falling through to its generic per-button-set guess - that generic guess (Cancel is safest for YesNoCancel) is actually the *destructive* choice for LoadOrderValidator's prompt specifically, contradicting IMergeNotifier's own non-destructive-default contract. This PR is what made LoadOrderValidator reachable from Core (and so, in principle, from a headless entry point) in the first place, so fixed at the root rather than left contingent on no headless caller ever reaching it. 3. InteractiveMergeRunner.MergeByTreeNodesAsync: moved TreeNode extraction back inside BackgroundWorker.DoWork instead of running it synchronously on the UI thread beforehand, matching the pre-split threading model. ExtractRequest dereferences node metadata with no null/type check; outside DoWork, an exception there now throws synchronously from btnMergeFiles_Click, which modern .NET WinForms terminates the process for by default - DoWork instead captures it into RunWorkerCompletedEventArgs.Error (still unread by OnMergeComplete, same as before this PR), so this restores the original "silently does nothing" failure mode rather than a crash. 4. CLAUDE.md: updated the Architecture section (opening description, folder map split into Core/host, a new "Interactive vs. headless split" subsection) and corrected the IMergeNotifier section's now- false claim that MainForm's translation is behavior-identical to the old direct calls (see fix 1). Full CLAUDE.md federation is a later unit's job - this just stops the doc from actively lying to it and to the other Wave-0 units reading it as ground truth in the meantime. 5. WitcherScriptMerger.Core.csproj now references the same DeadCodeDetection.ruleset the host project uses, so the ~20 files that moved to Core don't silently drop out of dead-code analysis. 6. Factored MergeTextInteractive/MergeTextHeadless's identical AddModToMerge bookkeeping (which sources to record, and which to skip because they're the output file itself or an intermediate bundle-merge byproduct) into one shared RecordMergedSources helper - this bookkeeping feeds MergeInventory.xml's hashes, which are load-bearing, so a future fix now has one call site, not two that can silently drift apart. 7. FileMerger's constructor now throws a clear ArgumentNullException if given a null IMergeEngine, instead of leaving AppState.MergeEngine's null default (unlike its siblings Notifier/Settings, which both get real defaults) to eventually surface as an unhandled NullReferenceException deep inside Merge()/MergeHeadless() the first time some future entry point (a test harness, the Linux CLI/MCP-only host planned for a later unit) constructs a FileMerger without going through Program.Main's startup sequence first. Re-verified after all seven: dotnet build clean (same 5 pre-existing warnings), dotnet format --verify-no-changes passes, CLI merge and the full MCP tool round-trip both still succeed against the scratch tree, GUI still launches and stays responsive. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GXAuGMLB44T5Zv5o5ZzKah
…gebox-notifier Route LoadOrderValidator's prompt through Program.Notifier
Add GitHub Actions CI (build + format check on PRs)
Research spike: WolvenKit as a QuickBMS/wcc_lite replacement
…n-doc Add Vortex extension design doc (Unit 4, design only)
Paired with a branch-protection change (required approving reviews: 2 -> 1, require_code_owner_reviews: true) so a single approval from a listed owner satisfies the review requirement, while approvals from other GitHub users (this is a public repo) no longer count on their own. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GXAuGMLB44T5Zv5o5ZzKah
…oject # Conflicts: # WitcherScriptMerger.Core/LoadOrder/LoadOrderValidator.cs # WitcherScriptMerger/Forms/MainForm.cs # WitcherScriptMerger/HeadlessMergeNotifier.cs # WitcherScriptMerger/IMergeNotifier.cs
Split into WitcherScriptMerger.Core + host GUI/CLI/MCP project
…g, dry-run, order-file audit merge_conflicts now validates relativePaths and orderOverrides keys against Paths.ModsDirectory (proper Path.GetFullPath-based prefix check, not naive StartsWith) before any scan or merge runs, rejecting absolute/UNC/`..`-escaping entries with a clear error. Neither value was actually joined into a filesystem path anywhere in this codebase - this closes no live traversal, it's defense-in-depth against that changing later, and it fixes the "silently matches nothing" gap for a malicious-looking relativePaths entry. Audited orderOverrides for other misuse: values were already whitelisted via ModFile.ContainsMod before reaching Path.Combine, but the validation had two real gaps - a partial/duplicate mod list would silently merge an incomplete or self-paired chain and still report success, and a single-remaining-real-source override (reachable once a file's already-merged output re-enters conflict.Mods as a pseudo-source) would report "merged" having done nothing. FileMerger.ResolveMergeOrder now requires at least two entries, no duplicates, and every real source mod covered (excluding the configured merged-mod name itself, matching scan_conflicts's own documented guidance for re-merging). This validation is shared with the `merge` CLI verb's --order-file, so a pre-existing partial order-file that used to be silently accepted now gets rejected per-file instead. Added merge_conflicts's dryRun mode: previews which conflicts would auto-solve without writing merged output, repacking a bundle, or modifying MergeInventory.xml. Distinct from scan_conflicts's alreadyResolved (which only re-checks existing merge records) since dryRun actually exercises the merge engine for currently-unresolved conflicts. Output is redirected under TempBundleContent instead of the real destination; MergeInventory.Load gained an allowSave flag since it can otherwise write to disk on its own (backfilling an old record's missing hash) before dryRun is ever consulted; a dry run now also predicts the same "output already exists, declined" outcome a real run would hit, so the two don't disagree on a conflict whose output is already on disk. Documented minimal required permissions in a new Mcp/CLAUDE.md and updated the root CLAUDE.md's MCP mode section. AI-assisted: implemented by Claude Code per repo convention (CONTRIBUTING.md). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GXAuGMLB44T5Zv5o5ZzKah
Harden MCP tools for minimal-rights operation
Adds DiffPlexMergeEngine, an in-process alternative to KDiff3MergeEngine built on the MIT-licensed DiffPlex package, plus WitcherScriptMerger.Tests (xunit), the repo's first automated test project. Not the default engine: DiffPlex's ThreeWayDiffer has a confirmed upstream bug that can produce internally inconsistent diff-block metadata on multi-edit conflicts (measured 0.35%-38.89% failure rate depending on edit density) - DiffPlexMergeEngine detects and safely refuses rather than trusting corrupted output, but this is a real reliability gap KDiff3 doesn't share. Also fixes two things code review and end-to-end testing surfaced that undermined this PR's own AppState.Settings laziness fix: Paths.cs's eager static field initializers, and a non-atomic Settings lazy-init race. AI-assisted development per CONTRIBUTING.md.
Add DiffPlex-based merge engine and a test project
New project (net10.0, no -windows suffix, references Core only) that wires up just the merge CLI verb and mcp server mode - no WinForms, no GUI fallback. Always uses DiffPlexMergeEngine (Core, no external binary), since KDiff3MergeEngine needs Win32 P/Invoke that stays host-only. Getting this actually working on Linux (verified via WSL2, not just a cross-compile check) surfaced and fixed real bugs in shared Core code: - ModFile.GetModNameFromPath hardcoded '\' to find a path segment, crashing every flat-file merge on Linux (Path.Combine uses '/' there). Fixed to use Path.DirectorySeparatorChar. - WsmMcpTools.MergeConflicts normalized client-supplied paths toward a hardcoded '\' too, silently breaking relativePaths/orderOverrides matching on Linux. Same fix. - QuickBms.GetBundleContentPaths returned null when QuickBMS wasn't found, which ModFileIndex.BuildAsync and FileMerger.GetUnpackedFiles both enumerated directly - unreachable on the WinForms host (always gated behind the combined Paths.ValidateDependencyPaths()) but a real NullReferenceException once WitcherScriptMerger.Headless legitimately scans without QuickBMS/wcc_lite configured. Returns Array.Empty<string>() instead now. - FileMerger.GetUnpackedFiles's vanilla-bundle search also threw DirectoryNotFoundException on a missing content/DLC folder (a scratch/ incomplete game tree) - now degrades to "no vanilla bundle found" instead. Split Paths.ValidateDependencyPaths() into ValidateTextMergeDependencies() and ValidateBundleDependencies() so a host without QuickBMS/wcc_lite can still scan/merge flat-file conflicts; bundle-category conflicts fail gracefully per-conflict instead (ModFileIndex.BuildAsync prints one clear message per scan and skips bundle checking rather than crashing or being noisy per-bundle). This also relaxes WsmMcpTools's MCP-tool gating for the WinForms host's own mcp verb, not just the new host - documented in CLAUDE.md. Verified: full build, existing test suite, and dotnet format whitespace all pass. Self-contained single-file win-x64 and linux-x64 publishes both succeed. Actually ran the published linux-x64 binary under WSL2 (real Linux kernel) for both the merge and mcp verbs against synthetic scratch mods, including the bundle-graceful-degradation path - not just a cross-compile check. Added matching win-x64/linux-x64 publish commands to CLAUDE.md (no existing .pubxml convention to follow). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GXAuGMLB44T5Zv5o5ZzKah
Author
|
Opened against the wrong repo by mistake (this branch is part of ongoing work on a downstream fork, TheValiantOne/WitcherScriptMerger, and was not intended for upstream). Apologies for the noise — closing. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
WitcherScriptMerger.Headless, a fourth project in the solution: a slim executable that only wires up themergeCLI verb andmcpserver mode — no WinForms reference at all,net10.0(no-windowssuffix), so it's architecturally buildable and runnable on Linux. This is the first concrete step toward "true headless operation for CLI/Agent interaction, focused on modded-gaming + Vortex workflows" from this batch's driving goal.WitcherScriptMerger.Coreonly. Always usesDiffPlexMergeEngine(Core, no external binary) — there's noKDiff3MergeEngineavailable here (it needsTools/KDiff3.cs's Win32 P/Invoke, which stays host-only) and noMergeEngineconfig switch, since this host has exactly one engine.Program.csmirrors the WinForms host'smerge/mcprouting but with no GUI branch — no args or an unrecognized command prints usage and exits 1. No[STAThread], noAttachConsoleP/Invoke (Windows-only, correctly left out)..bundlefiles, needing QuickBMS/wcc_lite — seedocs/decisions/bundle-format-replacement-spike.md, no cross-platform replacement exists) are unsupported by design:CheckBundleContentsdefaults tofalsein this host's ownApp.config, and if turned on anyway, bundle conflicts fail gracefully with a clear message instead of crashing.dotnet publishcommands toCLAUDE.md(no existing.pubxmlconvention in this repo to follow).Bugs found and fixed along the way
Getting this to actually run on Linux (not just cross-compile) surfaced real, previously-unreachable bugs in shared
WitcherScriptMerger.Corecode — the WinForms host is Windows-only, so nothing had exercised these paths before:ModFile.GetModNameFromPathhardcoded'\'to find a path segment. On Linux,Path.Combine-built paths use/, soIndexOf('\')always returned-1, andSubstring(0, -1)threwArgumentOutOfRangeException. Fixed to usePath.DirectorySeparatorChar.WsmMcpTools.MergeConflictsnormalized client-suppliedrelativePaths/orderOverridestoward a hardcoded'\'too — correct on Windows, silently wrong on Linux (whereModFile.RelativePathitself uses/). Fixed to normalize both possible separators toPath.DirectorySeparatorChar.NullReferenceExceptionon missing QuickBMS:QuickBms.GetBundleContentPathsreturnednullwhen QuickBMS couldn't be found; bothModFileIndex.BuildAsyncandFileMerger.GetUnpackedFilesenumerated that directly. Unreachable on the WinForms host (always gated behind the combinedPaths.ValidateDependencyPaths(), so QuickBMS is guaranteed present before this code runs) but a real crash once this new host legitimately scans without QuickBMS/wcc_lite configured. Now returnsArray.Empty<string>().DirectoryNotFoundExceptioninFileMerger.GetUnpackedFiles's vanilla-bundle search on a missingcontent/DLCfolder (a scratch/incomplete game tree) — now degrades to "no vanilla bundle found" instead.Paths.ValidateDependencyPaths()intoValidateTextMergeDependencies()/ValidateBundleDependencies()so a host without QuickBMS/wcc_lite can still scan/merge flat-file conflicts instead of failing outright. This also relaxesWsmMcpTools's MCP-tool dependency gating for the WinForms host's ownmcpverb, not just the new host — previously a missing QuickBMS/wcc_lite path made everyscan_conflicts/merge_conflictsMCP call fail outright even with zero bundle-category conflicts in the mods folder. Documented inCLAUDE.md.All changes and rationale are documented in a new "Headless host" section in
CLAUDE.md.Test plan
dotnet build WitcherScriptMerger.slnsucceeds (verified against both the working tree and agit archive-exported clean checkout, to confirm no untracked files are needed).dotnet test(existingWitcherScriptMerger.Testssuite, 28 tests) passes unchanged.dotnet format whitespace WitcherScriptMerger.sln --verify-no-changespasses.win-x64(WinForms host + new host) andlinux-x64(new host).WitcherScriptMerger.Headless.exedirectly against a synthetic scratch game/mods tree —mergeverb correctly auto-solved one conflict (UTF-16LE+BOM output matching vanilla) and skipped a genuine conflict (git/diff3-style markers written underDiffPlexConflicts/);mcpverb's full stdio round-trip (initialize→tools/list→tools/call) verified for all 4 tools via a hand-rolled Python client.linux-x64binary was actually run there (not just cross-compiled) — this is how bugs Fix KDiff3 & Nexus Mod Manager Compaitibility Issues Regarding Symbolic Links #1 and Latest version of the source code? #2 above were found. Confirmed on genuine Linux:mergeverb against a native-filesystem scratch tree (correct merge output, correct conflict markers),mcpverb's full 4-tool round-trip including amerge_conflictscall using forward-slash paths specifically to exercise the separator fix, and the bundle-graceful-degradation path (junk.bundlefile +CheckBundleContents=true+ no QuickBMS → clean warning, exit code 2, no crash)..bundlefile, since QuickBMS isn't available in this environment to construct/unpack a real one — a genuine bundle-vs-bundle conflict relies on code inspection of the fixes above rather than an end-to-end run (matches the existing WinForms host's own "bundle path is code-reviewed but not round-tripped" status).AI-assisted development: this PR was substantially produced by Claude Code, per
CONTRIBUTING.md's disclosure policy. All behavior described above was independently verified by actually running the built/published binaries (including on a real Linux runtime via WSL2) rather than assumed from reading the code.https://claude.ai/code/session_01GXAuGMLB44T5Zv5o5ZzKah