…drift detection, and upgrade (#2860)
Agent bundle manifests shipped by integration plugins were only applied
at install/adopt time. Later manifest changes deployed with the plugin
but never reconciled into the live agent row, causing silent drift
(e.g. a model bump shipped in the repo but production kept the old
model for 8+ days).
Root cause: agent diff/upgrade required an explicit filesystem path
but operators only knew the agent slug. BundleSource::resolve() treated
a slug as a path and failed with "Bundle path not found: <slug>".
Fix:
- AgentBundleDirectoryRegistry: new datamachine_agent_bundle_directories
filter lets integration plugins register their on-disk bundle
directories (bundle_slug => path). Stale/non-existent dirs are dropped.
- AgentBundleAbilityService::load_bundle_from_input: when the source is
neither a remote URL nor an existing local path, resolve it as a slug
through the registry. This makes diff/upgrade/inspect/validate/adopt/
rebase all accept slugs, not just paths.
- status(): manifest drift detection — loads the on-disk manifest and
compares bundle_version + tracked agent_config against the installed
row, surfacing has_manifest_drift + config_drift fields.
- reconcile_all(): iterates every installed bundle-backed agent and runs
the standard upgrade path against its registered on-disk manifest.
- CLI: agent diff/upgrade now accept <slug-or-path>; upgrade --all
reconciles every installed bundle-backed agent; status shows a drift
column and warnings.
Precedence rule (existing, now reachable): the 3-way artifact ledger
compares installed (base) vs current (live) vs target (manifest).
Clean artifacts auto-apply; locally modified artifacts where the target
differs are staged as conflicts for approval. Operator overrides on
preserve_local paths (model, provider, allowed_redirect_uris) survive
via AgentConfigArtifactProjector::preserve_local_paths(). No new
precedence system — the existing one was just unreachable without
slug-based discovery.
Summary
Fixes #2860
Agent bundle manifests shipped by integration plugins were only applied to the live agent DB row at install/adopt time. Subsequent manifest changes deployed with the plugin but never reconciled into the row, causing silent drift. Production case: a
default_modelbump (gpt-5.4-nano→gpt-5.5) shipped in the roadie repo on June 30 but the live agent row kept nano for 8+ days, causing real session failures.Compounding factor:
wp datamachine agent diff <slug>andagent upgrade <slug>errored with "Bundle path not found: <slug>" because those commands required an explicit filesystem path — operators only knew the agent slug.Investigation findings
Why "Bundle path not found" happens
agent diff,agent upgrade,agent inspect,agent validate,agent adopt, andagent rebaseall take a<path>positional argument. The source string flows throughBundleSource::resolve()(inc/Engine/Bundle/BundleSource.php:64), which treats any non-remote string as a local path and checksfile_exists($source). When the operator passes a slug likeroadie,file_exists("roadie")is false → WP_Error "Bundle path not found: roadie".Why there was no bundle discovery mechanism
There was no filter or registry for integration plugins to declare where their on-disk bundle directories live. The
datamachine_agent_bundle_directoriesfilter did not exist. Integration plugins (e.g. extrachill-roadie) ship bundles atbundles/<slug>/manifest.jsonbut had no way to tell Data Machine where they were — installation was a manualwp datamachine agent install <path>invocation with no automated discovery or reconcile-on-activate path.Why
source_ref/source_revisiondidn't helpThe stored
datamachine_bundle.source_ref/source_revisionfields on the agent row are provenance metadata (URL / git SHA for remote sources), not resolvable local paths. They are empty for locally-installed bundles and were never designed to be read back as a filesystem path for diff/upgrade.The existing precedence system was correct but unreachable
The 3-way artifact ledger (
installed_hash/current_hash/ target hash) already implements the right precedence:preserve_localpaths (model,provider,allowed_redirect_uris) → operator values always survive viaAgentConfigArtifactProjector::preserve_local_paths().The problem was purely that this system was unreachable without slug-based discovery — nobody could run
agent upgrade <slug>to trigger it.What changed
1. Bundle directory discovery —
AgentBundleDirectoryRegistryNew class (
inc/Engine/Bundle/AgentBundleDirectoryRegistry.php) with adatamachine_agent_bundle_directoriesfilter. Integration plugins register their bundle directories:Stale/non-existent directories are silently dropped. The registry resolves by bundle slug, by agent row, or by raw slug (tries direct match, then falls back to resolving through an installed agent's stored
bundle_slug).2. Slug resolution in
load_bundle_from_inputAgentBundleAbilityService::load_bundle_from_input()now checks: if the source is neither a remote URL nor an existing local path, treat it as a slug and resolve it through the registry. This makes all path-based commands (diff,upgrade,inspect,validate,adopt,rebase) accept slugs:3. Manifest drift detection in
statusagent status <slug>now loads the on-disk manifest (when discoverable) and comparesbundle_version+ the trackedagent_configpayload against the installed row. Returns:has_manifest_drift(bool) — quick signalmanifest.bundle_version.drift— version mismatchmanifest.config_drift— per-field{key, installed, manifest}entriesTable output shows a
driftcolumn and prints warnings for each drifted field.4. Reconcile verb —
agent upgrade <slug>and--allagent upgradenow accepts either a slug or a path. When given a slug, it resolves the on-disk bundle directory and runs the existing upgrade machinery (3-way merge via the artifact ledger). The--allflag iterates every installed bundle-backed agent and reconciles each one against its registered on-disk manifest.5. Tests
New smoke test (
tests/agent-bundle-manifest-reconcile-smoke.php, 34 assertions):config_drift_fieldsdetects changed/added/removed keys, ignores key ordertracked_payloadexcludesdatamachine_bundlefrom drift comparisonpreserve_local_paths(model key)Precedence rule (existing, documented here for clarity)
The reconcile path uses the existing 3-way artifact ledger. No new precedence system was built:
preserve_localkey (model, provider)This is the correct shape: operator overrides survive unless explicitly reset, manifest changes reach the row when the operator hasn't diverged, and genuine conflicts are surfaced for human review rather than silently clobbered.
Integration plugin follow-up
Integration plugins (extrachill-roadie, extrachill-event-bundles, etc.) need to hook
datamachine_agent_bundle_directoriesto register their bundle directories. This is a one-line change per plugin — out of scope for this Data Machine PR.