Turning a Theme-Bound Generative Art System into a Maintainable WordPress Plugin

I began this migration for a practical reason: a background feature that had started as a theme customization had grown into a modular browser-based geometry engine. It still worked, but the theme now owned administration, persistence, mosaics, twelve generators, rendering, and a public badge. I wanted a cleaner boundary without losing the behaviour already proven on the live site.

The result was a site-specific standalone WordPress plugin, a deliberately minimal theme fallback, and a workflow in which the current VPS remains the primary technical authority. The migration preserved thirteen configured visual records, the complete Generative Engine, the existing WordPress option, the distinction between visually different records sharing one media URL, and every runtime mode already in use.

This was not a generic plugin product and I did not intend to distribute it. It only needed to integrate correctly with one website and its current theme. That narrower scope gave me useful freedom: I could design around the real site instead of constructing an abstraction for hypothetical installations. At the same time, working on a production VPS demanded more discipline than an ordinary local refactor.

When a theme customization becomes an application

The original Background Studio had begun as a relatively small extension inside a classic WordPress theme. Over several iterations it acquired saved media records, random and static selection, weighted probabilities, page, session and daily persistence, reduced-motion behaviour, image and video rendering, mosaics, scattered repeats, colour controls and a browser-based Generative Engine.

By August 2026 the engine exposed twelve algorithms:

  • binary-space partition;
  • quadtree;
  • Hilbert ordering;
  • golden spiral partitioning;
  • Voronoi cells;
  • Delaunay triangulation;
  • Lloyd-relaxed Voronoi cells;
  • phyllotaxis;
  • radial fan sectors;
  • squarified treemap;
  • diagonal Truchet tessellation;
  • Ulam spiral ordering.

The mathematical generators registered with a common JavaScript engine. A rendering layer requested normalized regions from the selected generator and assigned saved Background Studio records to those regions. D3 Delaunay 6.0.4 was served locally for the geometry that required it; there was no runtime CDN dependency.

Automatic permutation could choose among all twelve algorithms. Manual selection remained available, and an optional automatic-complexity setting generated levels from one through five deterministically from the composition seed. The public badge displayed the actual selected algorithm, never the word AUTOMATIC, while complexity remained available in the runtime state without being printed publicly.

In other words, the theme had quietly acquired a second job as an application framework. It was doing the job surprisingly well, but that did not make the boundary sensible.

The earlier verified environment was an x86_64 Debian 13 VPS. The historical handoff recorded PHP 8.4.24, WordPress 7.0.4, Node.js 20.19.2 and a 6.12-series Debian cloud kernel. Those facts described a confirmed checkpoint; they were never treated as eternal properties of the server. Every later change began by inspecting the environment again.

The objectives that defined the migration

The primary objective was maintainability. Future work on the Generative Engine should not require extending an increasingly long chain of loaders inside the theme. A theme replacement or update should not silently remove the engine. Conversely, deactivating the plugin should expose an ordinary theme state that was easy to understand and test.

I wanted the inactive state to be visually unambiguous. When the Background Studio had not added its active class, the outer page canvas would be solid black with no inherited background image. Foreground containers, articles, navigation and typography would remain untouched. When the active class was present, all managed image, Mosaic, collage, scattered-panel and Generative rendering had to continue exactly as before.

I was willing to let the public site display that simple black canvas briefly during the cutover. Preserving uninterrupted generative rendering was less important than making the migration sequence easy to reason about. Honestly, a controlled black background is a very respectable maintenance mode; it does not spin, flash or submit a support ticket.

The following constraints remained authoritative:

  • preserve the serialized Background Studio option;
  • preserve all thirteen records and their ordered identifiers;
  • preserve the two deliberate Hilbert variants that shared one WebP URL but used different configured dimensions;
  • preserve all twelve algorithms and reduced-motion behaviour;
  • do not modify posts, uploads, media, credentials, database configuration or wp-config.php;
  • do not introduce JavaScript merely to create the black fallback;
  • do not edit both theme and plugin unless current source evidence required both;
  • build and validate candidates outside the live directories;
  • retain a timestamped rollback checkpoint for every material step.

The Hilbert variants deserve emphasis. A media URL was not the identity of a visual instrument. The record identifier, URL and visual configuration—including dimensions—formed its effective identity. Deduplicating solely by URL would have destroyed an intentional distinction and changed the resulting compositions.

Establishing what was actually live

Before designing the cutover, I performed a complete source audit. Historical documentation was useful, but it could not answer whether a file had subsequently changed, whether a loader still existed, or whether a plugin had already been introduced during an earlier attempt.

My authority order became simple: current live source and WordPress state first; parser, checksum, runtime and command output second; the written handoff third; earlier explanations last.

The audit covered the active theme’s functions.php and style.css, every inc/background*.php file, every non-minified js/background*.js file, every css/background*.css file, Additional CSS, relevant enqueue calls, the persistent option and the complete plugins directory.

A simplified version of the read-only inspection looked like this:

set -Eeuo pipefail

WP_ROOT="/var/www/example-site"
THEME_DIR="$WP_ROOT/wp-content/themes/example-theme"
PLUGIN_ROOT="$WP_ROOT/wp-content/plugins"

find "$THEME_DIR/inc" \
    -maxdepth 1 \
    -type f \
    -name 'background*.php' \
    -print

find "$THEME_DIR/js" \
    -maxdepth 2 \
    -type f \
    -name 'background*.js' \
    ! -name '*.min.js' \
    -print

find "$THEME_DIR/css" \
    -maxdepth 1 \
    -type f \
    -name 'background*.css' \
    -print

grep -RIn \
    --exclude='*.min.js' \
    'background-studio-active\|GenerativeState' \
    "$THEME_DIR" \
    "$PLUGIN_ROOT"

wp --allow-root \
    --path="$WP_ROOT" \
    plugin list

wp --allow-root \
    --path="$WP_ROOT" \
    eval '
        $value = get_option(
            "example_background_studio",
            array()
        );

        echo "Records: "
            . count($value["items"] ?? array())
            . "\n";

        echo "Mode: "
            . ($value["mode"] ?? "missing")
            . "\n";
    '

The audit established that no actual WordPress plugin contained Background Studio code. The complete system was theme-based. PHP syntax passed for the theme loader and all extension files, WordPress bootstrapped successfully, and the audit changed nothing.

This finding corrected an earlier broad assumption that both an existing plugin and the theme might need modification. There was no existing plugin implementation to protect or patch. The migration first needed to create one.

The audit also clarified the lifecycle of the active HTML class. The frontend selector added a class to the root html element after selecting a managed background. Theme CSS then applied custom properties to the root and made the body transparent so the selected background remained visible. Mosaic and Generative modes added their own layers and mode classes.

That meant the inactive fallback belonged in the theme’s public CSS. The plugin should own active rendering; the theme should define what the page looked like before or without that rendering. No database setting or JavaScript state was needed to express this boundary.

Drawing the new ownership boundary

The final architectural division was deliberately narrow. The standalone plugin owned the administration page, option handling, selection logic, Mosaic and collage rendering, separate panels, the Generative Engine, all twelve algorithms, the badge and the public assets. The theme retained only the inactive black canvas.

The fallback followed this conceptual form, with the deployed selector adapted to the site’s existing specificity and loading order:

html:not(.example-background-studio-active),
html:not(.example-background-studio-active) body {
    background-color: #000000 !important;
    background-image: none !important;
}

html.example-background-studio-active body {
    background-color: transparent !important;
    background-image: none !important;
}

The first rule applies only when the active class is absent. It changes the outer canvas, not #page, article elements, navigation or typography. It adds no layout space and therefore creates no cumulative layout shift.

The second half of the boundary existed in the new plugin bootstrap. The real bootstrap was built from the audited source structure, but its essential responsibility can be represented as follows:

<?php
/**
 * Plugin Name: Background Studio
 */

defined( 'ABSPATH' ) || exit;

define(
    'EXAMPLE_BACKGROUND_STUDIO_DIR',
    plugin_dir_path( __FILE__ )
);

define(
    'EXAMPLE_BACKGROUND_STUDIO_URL',
    plugin_dir_url( __FILE__ )
);

require_once
    EXAMPLE_BACKGROUND_STUDIO_DIR
    . 'inc/background-studio.php';

require_once
    EXAMPLE_BACKGROUND_STUDIO_DIR
    . 'inc/background-mosaic-extension.php';

require_once
    EXAMPLE_BACKGROUND_STUDIO_DIR
    . 'inc/background-generative-extension.php';

require_once
    EXAMPLE_BACKGROUND_STUDIO_DIR
    . 'inc/background-generative-badge-extension.php';

The important change was not simply moving files between directories. Theme-relative filesystem paths and asset URLs had to become plugin-relative paths and URLs. Loader order still mattered because algorithm modules registered with the common engine before the frontend renderer requested them. Administration dependencies also needed to preserve their existing enqueue sequence.

I did not attempt to make the plugin theme-agnostic. It was allowed to understand the current site’s foreground stacking, page container and black fallback contract. This avoided a large compatibility layer that would have brought no practical benefit.

Building and cutting over in reversible stages

The first plugin candidate was assembled under a timestamped /tmp directory. It contained 35 files. PHP syntax, JavaScript syntax and an explicit manifest passed before anything was copied into wp-content/plugins. A SHA-256 digest identified the candidate manifest, but the digest itself was evidence for that build, not a promise that future source would retain the same bytes.

The candidate-validation pattern was intentionally ordinary:

set -Eeuo pipefail

CANDIDATE="/tmp/background-plugin-candidate/background-studio"

php -l \
    "$CANDIDATE/background-studio.php"

find "$CANDIDATE/inc" \
    -type f \
    -name '*.php' \
    -print |
while IFS= read -r file; do
    php -l "$file"
done

find "$CANDIDATE/js" \
    -type f \
    -name '*.js' \
    ! -name '*.min.js' \
    -print |
while IFS= read -r file; do
    node --check "$file"
done

find "$CANDIDATE" \
    -type f \
    -print |
LC_ALL=C sort

The migration then proceeded through four bounded stages.

  1. Install the plugin inactive. The validated candidate was installed in the plugins directory without activating it. All plugin PHP was linted again from its installed path. WordPress still reported thirteen records and the same option checksum.
  2. Cut the theme back to its fallback responsibility. A timestamped theme checkpoint was created. The theme loader and active-renderer CSS were removed, and the black inactive fallback was installed. The plugin remained inactive, producing the deliberately simple black state.
  3. Activate and verify the plugin. WordPress activated the standalone plugin. Integration functions loaded, public assets responded, the configured mode remained generative, and the option checksum remained unchanged.
  4. Remove legacy theme ownership. Thirty-three old Background Studio files were moved out of the live theme into a separate timestamped checkpoint. Zero corresponding legacy files remained in the theme.

The state checksum was calculated from the serialized option rather than from a pretty-printed interpretation:

OPTION_HASH="$(
    wp --allow-root \
        --path="/var/www/example-site" \
        eval '
            $value = get_option(
                "example_background_studio",
                array()
            );

            echo hash(
                "sha256",
                serialize( $value )
            );
        '
)"

printf 'Option SHA-256: %s\n' "$OPTION_HASH"

This mattered because a record could remain visually plausible while a weight, identifier, dimension or persistence field had changed. Comparing only the number of items would have been a weak regression test.

Backups were created before every material stage. A backup is pessimism with a timestamp, and I mean that as praise. The rollback commands named exact files and exact directories; they did not depend on unresolved variables or broad recursive targets.

For multi-file source changes I used reviewed patches with an exact dry run:

patch \
    --dry-run \
    --fuzz=0 \
    -p1 \
    -d "$CANDIDATE_ROOT" \
    < "$PATCH_FILE"

patch \
    --fuzz=0 \
    -p1 \
    -d "$CANDIDATE_ROOT" \
    < "$PATCH_FILE"

When line-oriented patching was unsuitable, Python performed semantic or marker-based replacements with explicit count assertions:

from pathlib import Path

path = Path("/tmp/candidate/example.php")
text = path.read_text(encoding="utf-8")

start = "/* BEGIN MANAGED BLOCK */"
end = "/* END MANAGED BLOCK */"

if text.count(start) != 1:
    raise SystemExit(
        "Unexpected start-marker count"
    )

if text.count(end) != 1:
    raise SystemExit(
        "Unexpected end-marker count"
    )

old = text[text.index(start):text.index(end) + len(end)]
updated = text.replace(old, replacement, 1)

path.write_text(
    updated,
    encoding="utf-8",
)

This approach was idempotent and inspectable. A rerun replaced or recognized one managed block; it did not append duplicates. Blind sed replacement against production source was excluded because an unexpected match could quietly rewrite the wrong location.

The failures that improved the technical model

The final migration was clean because earlier iterations had already exposed several weak assumptions. Those failures were useful precisely because the installers stopped before deployment or restored their checkpoints afterward.

Exact source assumptions were too fragile

Several early installers searched for exact fragments that no longer matched the live source. Representative failures included:

Could not find Random method row.
Existing frontend enqueue call not found.
Expected one administration visibility function; found 0.

These were not random parser failures. They showed that the patchers had been designed around remembered source instead of inspected source. Later iterations used isolated extension files, verified loader anchors and marker counts. When a candidate could not prove its assumptions, it stopped.

An early diagnosis also found zero Mosaic markers even though previous terminal sessions had created checkpoints. The accurate conclusion was that the earlier attempts had backed up files but had not deployed the feature. Evidence replaced the more comforting story that “it probably installed.” Computers are unusually literal colleagues; they rarely infer our good intentions.

Client-side evidence changed a server-side decision

One subtle bug involved the classic three-background layout. JavaScript tests proved that classic_three had been selected, yet the page still rendered two regions. The browser was not lying: the original Mosaic renderer had received a server-localized count of two before the client made its later selection.

The correction moved classic-layout choice into PHP. The server temporarily altered the effective frontend option for that request without rewriting the saved database option. Classic one invoked the original random mode; classic two and three invoked the exact original Mosaic methods with counts two and three. A browser-side layout selector became a safe no-op.

This was an architectural correction driven by runtime evidence. Adding another JavaScript override would have treated the symptom while preserving the inconsistent state boundary.

Tool validation can fail even when source is valid

Node.js rejected a temporary JavaScript candidate because the temporary filename lacked a .js extension:

TypeError [ERR_UNKNOWN_FILE_EXTENSION]

The code was syntactically valid. The validator invocation was wrong. Later candidates used mktemp --suffix=.js before node --check.

Another post-deployment test used an unsupported WP-CLI format:

wp option get home --format=plaintext

The installed WP-CLI rejected that format value. The corrected test asked WordPress directly:

wp --allow-root \
    --path="/var/www/example-site" \
    eval 'echo home_url("/");'

A failed public-page test triggered the planned automatic rollback. That distinction mattered: the feature source had not necessarily failed, but the complete deployment contract had.

The patching environment also had a history

The VPS initially lacked GNU patch. After it was installed, one early patch failed with:

patch: **** malformed patch at line 692

Another valid Phase 2 patch failed because its functions.php hunk expected an obsolete line location. The correction generated a candidate from the actual file and inserted the loader through a verified source anchor.

When npm was unavailable, installing an entire package-management stack for one browser library seemed unnecessary. The official D3 package archive was retrieved, its package and version were verified, and the minified dependency plus licence were served locally.

A top-level set -euo pipefail also caused an invoked terminal session to close immediately on failure. The shell had obeyed with impressive moral certainty and almost no social grace. Later installers ran their work inside a child Bash heredoc, printed an explicit child exit code and kept useful logs visible.

macOS required its own compatibility discipline

The documentation repository and context-export helper lived on macOS Monterey, whose system Bash was 3.2. A repository-management script stopped at:

mapfile: command not found

mapfile arrived in Bash 4, so as far as the system shell was concerned it was a command from the future. I did not replace /bin/bash and did not install Homebrew merely to run the workflow. The scripts were rewritten using Bash 3.2-compatible while read loops.

A later README update appeared to stop at a lone colon while printing a long Git diff. Nothing had failed: Git had opened the output in less and was waiting for q. Even the documentation demanded one final keystroke. Future review commands should use git --no-pager diff when an unattended continuation is expected.

Separating different kinds of validation

One of the most useful methodological changes was to stop treating every successful command as the same kind of proof. The workflow distinguished several layers:

Validation layer What it established Representative mechanism
Syntax validation The language parser accepted an individual source file php -l and node --check
Structural validation Expected files, markers, loaders and CSS structure existed exactly once Manifest counts, marker assertions and brace checks
Semantic testing The algorithm produced finite, bounded and meaningful geometry Normalized-coordinate and coverage tests
Regression testing Persistent state and deliberate record identities survived Serialized option hash, count and ordered identifiers
Deployment verification The installed plugin loaded through WordPress and served its assets Plugin state, WordPress bootstrap and HTTP requests
Browser runtime testing The real page selected and displayed the intended mode Runtime state, root classes, badge and visual inspection

The geometry modules had already passed focused semantic tests. Examples included seventeen treemap regions covering the normalized viewport, fifty triangular regions from a Truchet grid of five, and forty-nine unique Ulam cells from a grid of seven.

TREEMAP PASS: regions=17
TRUCHET PASS: regions=50
ULAM-SPIRAL PASS: regions=49

After the plugin migration, WordPress still reported thirteen saved records and Generative mode. The installed plugin loaded its PHP integration and public assets. Thirty-three legacy theme files had been removed, and no legacy asset references remained.

The decisive browser test reported:

{
    activeClass: true,
    modeVersion: "5.0.0",
    engineVersion: "1.0.0",
    actualAlgorithm: "radial-fan",
    actualComplexity: 5,
    automaticComplexity: true,
    availableAlgorithms: 12,
    badge: "BG ENGINE RADIAL FAN",
    pluginAssets: 15,
    legacyThemeAssets: 0
}

The public page also passed visual inspection. When the plugin was active, the Generative Engine behaved as before. When its active class was absent, the theme exposed the solid black canvas without recolouring or hiding the foreground page.

The value actualAlgorithm was a concrete algorithm, never random or AUTOMATIC. That small assertion tested a larger architectural promise: automatic selection remained observable after it had made its decision.

Moving the handoff into a project-specific repository

Once the code no longer belonged to the theme, leaving its permanent handoff inside a general VPS migration repository felt equally awkward. I created a separate private repository for the Background Studio project, moved the complete handoff into its README.md, and replaced the old handoff with a short relocation notice.

The first documentation update stopped because git diff --check detected trailing whitespace in a newly inserted metadata line. No commit or push occurred. The whitespace was removed before the repository migration continued. This was a tiny defect, but it demonstrated why a documentation workflow deserves validation too.

The new repository was initially empty. That may have been the calmest component in the entire project. The complete 1,809-line handoff became its root README, and a second commit added an executable macOS helper named Copy-Project-Context.command.

The actual plugin source was not duplicated permanently into this repository. The live VPS remained authoritative, while a separate private WordPress backup repository contained a GitHub mirror of the current plugin directory. Automatically copying that directory into a second repository would have created two apparent sources of truth and complicated future development.

Instead, the helper performs a temporary sparse clone whenever I need to begin a new technical conversation:

git clone \
    --depth 1 \
    --branch main \
    --single-branch \
    --filter=blob:none \
    --sparse \
    "https://github.com/example-owner/wordpress-backup.git" \
    "$TEMP_DIR/wordpress-backup"

git -C "$TEMP_DIR/wordpress-backup" \
    sparse-checkout set \
    --cone \
    "website/wp-content/plugins/background-studio"

git -C "$PROJECT_DIR" \
    show origin/main:README.md \
    > "$TEMP_DIR/README.md"

pbcopy < "$TEMP_DIR/PROJECT-CONTEXT.txt"

The helper reads the latest project README from the project repository, fetches only the plugin directory from the backup mirror, generates a manifest containing paths, byte sizes and SHA-256 digests, concatenates every readable first-party source file and places the result in the macOS clipboard.

Minified vendor code is listed in the manifest but omitted from the prompt body. The confirmed run discovered 35 plugin files, included 34 source bodies and omitted one minified dependency. The resulting context contained 397,858 bytes and 14,947 lines. Temporary files were removed when the command finished; no persistent plugin copy remained on the Mac.

A second test came from double-clicking the .command file in Finder. It repeated the sparse retrieval, included the complete README and current plugin source, copied the result to the clipboard and exited with code zero. This gave me something close to a project button without introducing a browser extension, a cross-repository token or a generated source bundle committed to Git.

Why I kept the VPS as the present source of truth

A conventional software project would usually place its canonical source in a dedicated repository, build releases in CI and deploy those releases to production. That remains a reasonable future direction. It was not the state of this project during the migration.

The current plugin had grown through careful iterations performed against one live WordPress installation. The WordPress backup repository mirrored that installation, while the project repository held the technical handoff and context helper. Treating the new repository as canonical before importing, comparing and validating the complete live source would have reversed the evidence hierarchy prematurely.

For the present workflow, every material change therefore begins with a fresh VPS inspection. The administrator runs plain, inspectable Bash in the authorized VPS terminal. Candidates are built under a unique /tmp directory on the VPS, tested there and installed only after validation.

The project guidance records the exact general procedure:

  1. inspect the current live plugin, relevant theme integration, option, records, loaders and runtime;
  2. record checksums before changing anything;
  3. create a timestamped backup and rollback command;
  4. build candidates outside the live plugin;
  5. use an exact patch dry run or a marker-counted Python replacement;
  6. validate PHP, JavaScript, CSS, WordPress bootstrap and feature semantics;
  7. install only the validated candidate;
  8. compare the option checksum, record count and ordered identifiers;
  9. test public assets, HTML classes, runtime state and the browser result;
  10. print the installed checksums, backup path, rollback command and final exit code.

At the time of the recorded output, the command adding this guidance to the README had reached its reviewed Git diff and opened the pager. The final commit and push were not yet evidenced in the captured terminal output, so I would not describe them as confirmed until the subsequent Git result appeared. That may sound pedantic, but the whole method depends on refusing to turn an expected result into a reported fact.

What the migration changed and what it did not

The plugin migration changed ownership, loading paths and maintainability. It did not redesign the artwork, alter probabilities, rewrite saved options or regenerate media. The Generative Engine remained a browser-side system using the existing record library as its visual vocabulary.

The final boundary was clear:

  • the theme owned the inactive black fallback;
  • the plugin owned active background selection and rendering;
  • the persistent WordPress option retained its existing records and settings;
  • the VPS remained the authority for current implementation decisions;
  • the backup repository supplied a convenient mirror;
  • the project repository preserved history, method and reusable context.

This also reduced the risk associated with future theme maintenance. Replacing or updating the theme could still affect foreground integration or the fallback, but it would no longer remove the complete art engine simply because its files happened to live under the theme directory.

Remaining limitations and future improvements

No active installation fault was known at the stopping point, but several practical limitations remained.

The backup mirror can lag behind the VPS. The clipboard helper is therefore excellent for conversation context but cannot replace a fresh live audit before patching. A future workflow could compare the backup commit with a live manifest and report whether the mirror is current.

The plugin is still designed for one theme and one site. This is intentional, although its theme contract should remain documented. If the theme changes, the stacking of #page, the inactive fallback and any foreground transparency assumptions will need regression testing.

Animated GIFs remain expensive. Browser caching may prevent repeated downloads of one URL, but every visible animated layer still has decoding and rendering cost. Dense Truchet, Ulam or scattered compositions can also produce more DOM regions or CSS layers than low-complexity layouts. Desktop syntax tests cannot measure mobile battery use, memory pressure or perceived smoothness.

Cache layers can obscure a correct deployment. Browser caches, page caches and a CDN may temporarily serve older HTML or assets. Runtime testing must distinguish a stale response from a source defect before another patch is invented to “fix” code the browser has not loaded yet.

The context export is large—almost 400 KB in the confirmed run. It is useful for a capable context window, but future tooling could offer two modes: a complete export and a focused export containing only files related to a proposed change.

Eventually I may import the verified plugin source into the project repository and make it canonical. That would support tagged releases, deterministic packaging, CI syntax checks and deployment from a reviewed commit. Such a transition should happen once, with a live-source comparison and explicit authority change change. Quietly allowing two repositories to compete would be easier to automate and harder to trust.

Automated browser assertions would also be valuable. A small test suite could verify that the root active class appears, the actual algorithm is never reported as random, the badge names that algorithm, the expected number of generators is registered and no legacy theme asset is loaded.

What I learned from the human–AI workflow

AI assistance was valuable throughout the project, but it did not replace evidence or judgment. The AI helped generate source candidates, patchers, validators, audit commands and documentation. Deterministic tools decided whether PHP parsed, JavaScript parsed, patches matched, geometry remained bounded, checksums changed or WordPress bootstrapped. The live browser showed what users actually received. I decided which trade-offs were acceptable and executed every production command.

This division of labour matters. A plausible explanation is not the same as a source inspection. A well-written patch is not a deployed feature. A successful syntax check is not a runtime test. A checkpoint directory is not proof that the attempted installation reached production.

The workflow became increasingly useful as it became increasingly inspectable. Plain Bash, explicit paths, candidate directories, marker counts, file manifests, rollback commands and exit codes gave me ways to understand and challenge the proposed actions. Human oversight worked because the process produced evidence I could read, not because a generic instruction said “keep a human in the loop.”

Reversibility also changed the quality of decision-making. Once each step had a bounded target, validated candidate and precise rollback, it became easier to make meaningful changes without pretending that uncertainty had disappeared. The goal was controlled uncertainty, not omniscience—which, to be fair, is already an ambitious feature request.

The migration ultimately succeeded because architecture and method reinforced each other. The plugin boundary reduced coupling. The black fallback provided a clear inactive state. The audit established what was real. Checksums protected persistent state. Runtime evidence corrected mistaken assumptions. The project repository preserved the reasoning, and the macOS helper made that reasoning reusable without creating another uncontrolled source copy.

What began as a background effect had become a serious little software system. Treating it accordingly did not require a large framework or an elaborate deployment platform. It required clear ownership, current evidence, small reversible steps and the patience to let a failed check change the plan.