@openclawbrain/cli 0.4.20 → 0.4.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @openclawbrain/cli
2
2
 
3
- Operator CLI for OpenClawBrain. Use it with `@openclawbrain/openclaw`.
3
+ Operator CLI internals for OpenClawBrain.
4
4
 
5
5
  The public front door is one command pinned to one OpenClaw home:
6
6
 
@@ -20,6 +20,8 @@ openclawbrain proof --openclaw-home ~/.openclaw
20
20
 
21
21
  The intended canonical lane is the same install command with optional `--proof`. Until that lands cleanly across the operator surfaces, proof stays a separate follow-up command. `proof` writes `summary.md`, `steps.json`, `verdict.json`, raw step logs, and proof pointers under one bundle directory.
22
22
 
23
+ This package is part of the internal split architecture. Public docs should lead with OpenClawBrain and the `openclawbrain install` lane, not with package-pair trivia.
24
+
23
25
  ## Common commands
24
26
 
25
27
  ```bash
package/dist/src/cli.js CHANGED
@@ -6,7 +6,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
6
6
  const __filename = fileURLToPath(import.meta.url);
7
7
  const __dirname = path.dirname(__filename);
8
8
  import { DEFAULT_OLLAMA_EMBEDDING_MODEL, createOllamaEmbedder } from "@openclawbrain/compiler";
9
- import { ensureManagedLearnerServiceForActivationRoot, inspectManagedLearnerService, removeManagedLearnerServiceForActivationRoot, parseDaemonArgs, runDaemonCommand } from "./daemon.js";
9
+ import { describeManagedLearnerServiceRuntimeGuard, ensureManagedLearnerServiceForActivationRoot, inspectManagedLearnerService, removeManagedLearnerServiceForActivationRoot, parseDaemonArgs, runDaemonCommand } from "./daemon.js";
10
10
  import { exportBrain, importBrain } from "./import-export.js";
11
11
  import { buildNormalizedEventExport } from "@openclawbrain/contracts";
12
12
  import { buildTeacherSupervisionArtifactsFromNormalizedEventExport, createAlwaysOnLearningRuntimeState, describeAlwaysOnLearningRuntimeState, drainAlwaysOnLearningRuntime, loadOrInitBaseline, materializeAlwaysOnLearningCandidatePack, persistBaseline } from "./local-learner.js";
@@ -15,7 +15,7 @@ import { resolveActivationRoot } from "./resolve-activation-root.js";
15
15
  import { describeOpenClawHomeInspection, discoverOpenClawHomes, formatOpenClawHomeLayout, formatOpenClawHomeProfileSource, inspectOpenClawHome } from "./openclaw-home-layout.js";
16
16
  import { inspectOpenClawBrainHookStatus, inspectOpenClawBrainPluginAllowlist } from "./openclaw-hook-truth.js";
17
17
  import { describeOpenClawBrainInstallIdentity, describeOpenClawBrainInstallLayout, findInstalledOpenClawBrainPlugin, getOpenClawBrainKnownPluginIds, normalizeOpenClawBrainPluginsConfig, pinInstalledOpenClawBrainPluginActivationRoot, resolveOpenClawBrainInstallTarget } from "./openclaw-plugin-install.js";
18
- import { buildOpenClawBrainConvergeRestartPlan, classifyOpenClawBrainConvergeVerification, describeOpenClawBrainConvergeChangeReasons, diffOpenClawBrainConvergeRuntimeFingerprint, finalizeOpenClawBrainConvergeResult, planOpenClawBrainConvergePluginAction } from "./install-converge.js";
18
+ import { buildOpenClawBrainConvergeRestartPlan, classifyOpenClawBrainConvergeVerification, describeOpenClawBrainConvergeChangeReasons, diffOpenClawBrainConvergeRuntimeFingerprint, finalizeOpenClawBrainConvergeResult, planOpenClawBrainConvergePluginAction, shouldReplaceOpenClawBrainInstallBeforeConverge } from "./install-converge.js";
19
19
  import { loadAttachmentPolicyDeclaration, resolveEffectiveAttachmentPolicyTruth, writeAttachmentPolicyDeclaration } from "./attachment-policy-truth.js";
20
20
  import { DEFAULT_WATCH_POLL_INTERVAL_SECONDS, buildNormalizedEventExportFromScannedEvents, bootstrapRuntimeAttach, buildOperatorSurfaceReport, clearOpenClawProfileRuntimeLoadProof, compileRuntimeContext, createAsyncTeacherLiveLoop, createOpenClawLocalSessionTail, createRuntimeEventExportScanner, describeCurrentProfileBrainStatus, formatOperatorRollbackReport, listOpenClawProfileRuntimeLoadProofs, loadRuntimeEventExportBundle, loadWatchTeacherSnapshotState, persistWatchTeacherSnapshot, rollbackRuntimeAttach, resolveAttachmentRuntimeLoadProofsPath, resolveOperatorTeacherSnapshotPath, resolveAsyncTeacherLiveLoopSnapshotPath, resolveWatchSessionTailCursorPath, resolveWatchStateRoot, resolveWatchTeacherSnapshotPath, scanLiveEventExport, scanRecordedSession, summarizeLearningPathFromMaterialization, summarizeNormalizedEventExportLabelFlow, summarizeTeacherNoArtifactCycle, writeScannedEventExportBundle } from "./index.js";
21
21
  import { appendLearningUpdateLogs } from "./learning-spine.js";
@@ -31,6 +31,7 @@ const OPENCLAWBRAIN_EMBEDDER_BASE_URL_ENV = "OPENCLAWBRAIN_EMBEDDER_BASE_URL";
31
31
  const OPENCLAWBRAIN_EMBEDDER_PROVIDER_ENV = "OPENCLAWBRAIN_EMBEDDER_PROVIDER";
32
32
  const OPENCLAWBRAIN_EMBEDDER_MODEL_ENV = "OPENCLAWBRAIN_EMBEDDER_MODEL";
33
33
  const OPENCLAWBRAIN_INSTALL_SKIP_EMBEDDER_PROVISION_ENV = "OPENCLAWBRAIN_INSTALL_SKIP_EMBEDDER_PROVISION";
34
+ const LEGACY_COMPAT_PACKAGE_NAME = "@jonathangu/openclawbrain";
34
35
  const INSTALL_COMPATIBLE_LOCAL_TEACHER_MODEL_PREFIXES = [
35
36
  "qwen3.5:9b",
36
37
  "qwen3.5:8b",
@@ -1621,6 +1622,13 @@ function readInstallRuntimeFingerprint(openclawHome) {
1621
1622
  function runOpenClawBrainConvergePluginStep(openclawHome) {
1622
1623
  const before = readInstallRuntimeFingerprint(openclawHome);
1623
1624
  const plan = planOpenClawBrainConvergePluginAction(before);
1625
+ let uninstallCapture = null;
1626
+ if (plan.action === "install" && shouldReplaceOpenClawBrainInstallBeforeConverge(before)) {
1627
+ uninstallCapture = runCapturedExternalCommand("openclaw", ["plugins", "uninstall", plan.pluginId]);
1628
+ if (uninstallCapture.error !== null || uninstallCapture.exitCode !== 0) {
1629
+ throw new Error(`OpenClaw plugin-manager uninstall failed while migrating ${path.resolve(openclawHome)} onto the canonical plugin lane. Tried \`${uninstallCapture.shellCommand}\`. Detail: ${summarizeCapturedCommandFailure(uninstallCapture)}`);
1630
+ }
1631
+ }
1624
1632
  const commandArgs = plan.action === "install"
1625
1633
  ? ["plugins", "install", plan.packageSpec]
1626
1634
  : ["plugins", "update", plan.pluginId];
@@ -1651,7 +1659,7 @@ function runOpenClawBrainConvergePluginStep(openclawHome) {
1651
1659
  changed: diff.changed,
1652
1660
  changeReasons: diff.reasons,
1653
1661
  detail: diff.changed
1654
- ? `${plan.action === "install" ? "Installed" : "Refreshed"} plugin-manager state: ${describeOpenClawBrainConvergeChangeReasons(diff.reasons)}`
1662
+ ? `${uninstallCapture !== null ? "Replaced legacy/plugin-shadow install and " : ""}${plan.action === "install" ? "installed" : "refreshed"} plugin-manager state: ${describeOpenClawBrainConvergeChangeReasons(diff.reasons)}`
1655
1663
  : `${plan.action === "install" ? "Ran install" : "Ran update"} through the OpenClaw plugin manager, but no runtime-affecting plugin delta was detected`,
1656
1664
  warning: null,
1657
1665
  capture,
@@ -1722,6 +1730,27 @@ function buildAttachCommand(openclawHome, activationRoot = null) {
1722
1730
  function buildInstallEmbedderProvisionCommand(baseUrl, model) {
1723
1731
  return `OLLAMA_HOST=${quoteShellArg(baseUrl)} ollama pull ${quoteShellArg(model)}`;
1724
1732
  }
1733
+ function buildCanonicalInstallRecoveryPath(parsed) {
1734
+ return [
1735
+ buildInstallCommand(parsed.openclawHome),
1736
+ "openclaw gateway restart",
1737
+ buildInstallStatusCommand(parsed.activationRoot),
1738
+ buildProofCommandForOpenClawHome(parsed.openclawHome)
1739
+ ].join(" -> ");
1740
+ }
1741
+ function inspectInstallGuardrailWarnings(parsed) {
1742
+ const warnings = [];
1743
+ const runtimeFingerprint = readInstallRuntimeFingerprint(parsed.openclawHome);
1744
+ if (runtimeFingerprint.selectedInstall?.packageName === LEGACY_COMPAT_PACKAGE_NAME) {
1745
+ warnings.push(`Detected legacy compatibility plugin state (${LEGACY_COMPAT_PACKAGE_NAME}) for this OpenClaw home. Install is converging it back onto the single supported OpenClawBrain path.`);
1746
+ }
1747
+ const learnerInspection = inspectManagedLearnerService(parsed.activationRoot);
1748
+ const learnerRuntimeGuard = describeManagedLearnerServiceRuntimeGuard(learnerInspection);
1749
+ if (learnerRuntimeGuard.state !== "ok") {
1750
+ warnings.push(`${learnerRuntimeGuard.detail} Canonical recovery path: ${buildCanonicalInstallRecoveryPath(parsed)}`);
1751
+ }
1752
+ return warnings;
1753
+ }
1725
1754
  function describeExecOutput(value) {
1726
1755
  if (typeof value === "string") {
1727
1756
  const normalized = value.trim();
@@ -3135,6 +3164,7 @@ function installExtensionFromLocalWorkspaceBuild(extensionDir) {
3135
3164
  return [...LOCAL_WORKSPACE_EXTENSION_PACKAGES];
3136
3165
  }
3137
3166
  let cachedOpenClawPackageMetadata = null;
3167
+ let cachedOpenClawRuntimePackageMetadata = null;
3138
3168
  function resolveOpenClawPackageManifestPath() {
3139
3169
  const candidates = [
3140
3170
  path.resolve(__dirname, "..", "package.json"),
@@ -3163,6 +3193,34 @@ function readOpenClawPackageMetadata() {
3163
3193
  cachedOpenClawPackageMetadata = { name, version };
3164
3194
  return cachedOpenClawPackageMetadata;
3165
3195
  }
3196
+ function resolveOpenClawRuntimePackageManifestPath() {
3197
+ const candidates = [
3198
+ path.resolve(__dirname, "..", "..", "..", "openclaw", "package.json"),
3199
+ path.resolve(__dirname, "..", "..", "..", "..", "packages", "openclaw", "package.json"),
3200
+ ];
3201
+ for (const candidate of candidates) {
3202
+ if (existsSync(candidate)) {
3203
+ return candidate;
3204
+ }
3205
+ }
3206
+ throw new Error("OpenClawBrain runtime package manifest not found. Searched:\n" +
3207
+ candidates.map((candidate) => ` - ${candidate}`).join("\n"));
3208
+ }
3209
+ function readOpenClawRuntimePackageMetadata() {
3210
+ if (cachedOpenClawRuntimePackageMetadata !== null) {
3211
+ return cachedOpenClawRuntimePackageMetadata;
3212
+ }
3213
+ const manifestPath = resolveOpenClawRuntimePackageManifestPath();
3214
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
3215
+ const name = typeof manifest.name === "string" && manifest.name.trim().length > 0
3216
+ ? manifest.name.trim()
3217
+ : "@openclawbrain/openclaw";
3218
+ const version = typeof manifest.version === "string" && manifest.version.trim().length > 0
3219
+ ? manifest.version.trim()
3220
+ : "0.0.0";
3221
+ cachedOpenClawRuntimePackageMetadata = { name, version };
3222
+ return cachedOpenClawRuntimePackageMetadata;
3223
+ }
3166
3224
  function buildExtensionIndexTs(activationRoot) {
3167
3225
  const templatePath = resolveExtensionTemplatePath();
3168
3226
  const template = readFileSync(templatePath, "utf8");
@@ -3170,6 +3228,10 @@ function buildExtensionIndexTs(activationRoot) {
3170
3228
  }
3171
3229
  function buildExtensionPackageJson() {
3172
3230
  const packageMetadata = readOpenClawPackageMetadata();
3231
+ const runtimePackageMetadata = readOpenClawRuntimePackageMetadata();
3232
+ const dependencies = {
3233
+ [runtimePackageMetadata.name]: runtimePackageMetadata.version
3234
+ };
3173
3235
  return JSON.stringify({
3174
3236
  name: "openclawbrain",
3175
3237
  version: packageMetadata.version,
@@ -3178,9 +3240,7 @@ function buildExtensionPackageJson() {
3178
3240
  openclaw: {
3179
3241
  extensions: ["index.ts"]
3180
3242
  },
3181
- dependencies: {
3182
- [packageMetadata.name]: packageMetadata.version
3183
- }
3243
+ dependencies
3184
3244
  }, null, 2) + "\n";
3185
3245
  }
3186
3246
  function buildExtensionPluginManifest() {
@@ -3967,8 +4027,10 @@ function emitInstallConvergeResult(result, parsed) {
3967
4027
  function runInstallCommand(parsed) {
3968
4028
  let pluginResult = null;
3969
4029
  let attachResult = null;
4030
+ let guardrailWarnings = [];
3970
4031
  try {
3971
4032
  validateOpenClawHome(parsed.openclawHome);
4033
+ guardrailWarnings = inspectInstallGuardrailWarnings(parsed);
3972
4034
  pluginResult = runOpenClawBrainConvergePluginStep(parsed.openclawHome);
3973
4035
  attachResult = executeProfileHookAttachCommand(parsed);
3974
4036
  }
@@ -3976,7 +4038,7 @@ function runInstallCommand(parsed) {
3976
4038
  const verdict = finalizeOpenClawBrainConvergeResult({
3977
4039
  stepFailure: toErrorMessage(error),
3978
4040
  verification: null,
3979
- warnings: []
4041
+ warnings: guardrailWarnings
3980
4042
  });
3981
4043
  const failureResult = {
3982
4044
  command: "install",
@@ -4042,6 +4104,7 @@ function runInstallCommand(parsed) {
4042
4104
  restartPerformed: restartPlan.required && restartPlan.automatic && restartError === null
4043
4105
  });
4044
4106
  const convergeWarnings = [];
4107
+ convergeWarnings.push(...guardrailWarnings);
4045
4108
  if (pluginResult.warning) {
4046
4109
  convergeWarnings.push(pluginResult.warning);
4047
4110
  }
@@ -23,6 +23,7 @@ const DEFAULT_SCAN_ROOT_DIRNAME = "event-exports";
23
23
  const BASELINE_STATE_BASENAME = "baseline-state.json";
24
24
  const SCANNER_CHECKPOINT_BASENAME = ".openclawbrain-scanner-checkpoint.json";
25
25
  const CLI_PACKAGE_NAME = "@openclawbrain/cli";
26
+ const LEGACY_COMPAT_PACKAGE_NAME = "@jonathangu/openclawbrain";
26
27
  const CLI_BIN_NAME = "openclawbrain";
27
28
  const DEFAULT_DAEMON_COMMAND_RUNNER = (command) => execSync(command, {
28
29
  encoding: "utf8",
@@ -198,6 +199,26 @@ function resolveCliPackageRoot(startDir) {
198
199
  }
199
200
  return null;
200
201
  }
202
+ function readNearestPackageMetadataForPath(filePath) {
203
+ if (typeof filePath !== "string" || filePath.trim().length === 0) {
204
+ return null;
205
+ }
206
+ let currentDir = path.dirname(path.resolve(filePath));
207
+ while (true) {
208
+ const packageMetadata = readPackageMetadata(currentDir);
209
+ if (packageMetadata !== null) {
210
+ return {
211
+ root: currentDir,
212
+ ...packageMetadata,
213
+ };
214
+ }
215
+ const parentDir = path.dirname(currentDir);
216
+ if (parentDir === currentDir) {
217
+ return null;
218
+ }
219
+ currentDir = parentDir;
220
+ }
221
+ }
201
222
  function resolveDaemonPackageManagerLaunchSpec(moduleDir) {
202
223
  const cliPackageRoot = resolveCliPackageRoot(moduleDir);
203
224
  const cliPackageMetadata = readPackageMetadata(cliPackageRoot);
@@ -244,14 +265,47 @@ function describeDaemonProgramArguments(programArguments) {
244
265
  ? programArguments[1]
245
266
  : programArguments[0];
246
267
  const runtimePackageSpec = programArguments.find((argument) => argument.startsWith("--package="))?.slice("--package=".length) ?? null;
268
+ const runtimePackageMetadata = runtimePath === null ? null : readNearestPackageMetadataForPath(runtimePath);
247
269
  return {
248
270
  configuredProgramArguments: programArguments,
249
271
  configuredCommand: formatCommand(programArguments),
250
272
  configuredRuntimePath: runtimePath,
251
273
  configuredRuntimePackageSpec: runtimePackageSpec,
274
+ configuredRuntimePackageName: runtimePackageMetadata?.name ?? null,
275
+ configuredRuntimePackageVersion: runtimePackageMetadata?.version ?? null,
252
276
  configuredRuntimeLooksEphemeral: runtimePath === null ? null : isNpxCachePath(runtimePath)
253
277
  };
254
278
  }
279
+ export function describeManagedLearnerServiceRuntimeGuard(inspection) {
280
+ if (inspection.installed !== true || inspection.configuredProgramArguments === null) {
281
+ return {
282
+ state: "ok",
283
+ reason: "not_installed",
284
+ detail: "No managed learner service is installed yet."
285
+ };
286
+ }
287
+ if (inspection.configuredRuntimeLooksEphemeral === true) {
288
+ return {
289
+ state: "refresh_required",
290
+ reason: "ephemeral_runtime",
291
+ detail: `Learner service still points at an ephemeral runtime path (${inspection.configuredRuntimePath ?? "unknown"}). Refresh it onto the durable OpenClawBrain CLI path before trusting background learning.`
292
+ };
293
+ }
294
+ const runtimePackageName = inspection.configuredRuntimePackageName;
295
+ const runtimePackageSpec = inspection.configuredRuntimePackageSpec;
296
+ if (runtimePackageName === LEGACY_COMPAT_PACKAGE_NAME || runtimePackageSpec?.startsWith(`${LEGACY_COMPAT_PACKAGE_NAME}@`) || runtimePackageSpec === LEGACY_COMPAT_PACKAGE_NAME) {
297
+ return {
298
+ state: "refresh_required",
299
+ reason: "legacy_compat_runtime",
300
+ detail: `Learner service still points at the retired compatibility package ${LEGACY_COMPAT_PACKAGE_NAME}${inspection.configuredRuntimePath ? ` (${inspection.configuredRuntimePath})` : ""}. Refresh it onto the single supported OpenClawBrain CLI path before trusting background learning.`
301
+ };
302
+ }
303
+ return {
304
+ state: "ok",
305
+ reason: "durable_runtime",
306
+ detail: `Learner service points at the durable ${CLI_PACKAGE_NAME} runtime.`
307
+ };
308
+ }
255
309
  function resolveDaemonProgramArguments() {
256
310
  for (const candidate of getOpenclawbrainCliScriptPathCandidates()) {
257
311
  const cliScriptPath = resolveCliScriptCandidate(candidate);
@@ -465,7 +519,8 @@ export function inspectManagedLearnerService(activationRoot) {
465
519
  }
466
520
  export function ensureManagedLearnerServiceForActivationRoot(activationRoot) {
467
521
  const inspection = inspectManagedLearnerServiceInternal(activationRoot);
468
- if (inspection.matchesRequestedActivationRoot === true && inspection.running) {
522
+ const runtimeGuard = describeManagedLearnerServiceRuntimeGuard(inspection);
523
+ if (inspection.matchesRequestedActivationRoot === true && inspection.running && runtimeGuard.state === "ok") {
469
524
  return {
470
525
  state: "ensured",
471
526
  reason: "already_running_exact_root",
@@ -473,12 +528,25 @@ export function ensureManagedLearnerServiceForActivationRoot(activationRoot) {
473
528
  inspection
474
529
  };
475
530
  }
531
+ if (inspection.installed && runtimeGuard.state !== "ok") {
532
+ const stopResult = stopManagedLearnerService(activationRoot);
533
+ if (!stopResult.ok) {
534
+ return {
535
+ state: "deferred",
536
+ reason: "stale_runtime_refresh_failed",
537
+ detail: `${runtimeGuard.detail} Tried to stop the stale learner service first, but that failed: ${stopResult.message}`,
538
+ inspection: stopResult.inspection
539
+ };
540
+ }
541
+ }
476
542
  const startResult = startManagedLearnerService(activationRoot);
477
543
  if (startResult.ok) {
478
544
  return {
479
- state: "started",
480
- reason: "started_exact_root",
481
- detail: `Started the background learner service for ${startResult.inspection.requestedActivationRoot}; passive learning can begin for this attached profile now.`,
545
+ state: runtimeGuard.state !== "ok" ? "refreshed" : "started",
546
+ reason: runtimeGuard.state !== "ok" ? runtimeGuard.reason : "started_exact_root",
547
+ detail: runtimeGuard.state !== "ok"
548
+ ? `${runtimeGuard.detail} Refreshed the learner daemon onto the durable OpenClawBrain CLI path for ${startResult.inspection.requestedActivationRoot}.`
549
+ : `Started the background learner service for ${startResult.inspection.requestedActivationRoot}; passive learning can begin for this attached profile now.`,
482
550
  inspection: startResult.inspection
483
551
  };
484
552
  }
@@ -940,11 +1008,21 @@ export function daemonStatus(activationRoot, json) {
940
1008
  }
941
1009
  if (daemonLaunchDescription.configuredRuntimePath !== null) {
942
1010
  const runtimePackageSuffix = daemonLaunchDescription.configuredRuntimePackageSpec === null
943
- ? ""
1011
+ ? daemonLaunchDescription.configuredRuntimePackageName === null
1012
+ ? ""
1013
+ : ` (${daemonLaunchDescription.configuredRuntimePackageName}${daemonLaunchDescription.configuredRuntimePackageVersion === null ? "" : `@${daemonLaunchDescription.configuredRuntimePackageVersion}`})`
944
1014
  : ` (${daemonLaunchDescription.configuredRuntimePackageSpec})`;
945
1015
  const runtimeWarning = daemonLaunchDescription.configuredRuntimeLooksEphemeral ? " [ephemeral]" : "";
946
1016
  console.log(` Runtime: ${daemonLaunchDescription.configuredRuntimePath}${runtimePackageSuffix}${runtimeWarning}`);
947
1017
  }
1018
+ const runtimeGuard = describeManagedLearnerServiceRuntimeGuard({
1019
+ installed: plistInstalled,
1020
+ configuredProgramArguments,
1021
+ ...daemonLaunchDescription,
1022
+ });
1023
+ if (runtimeGuard.state !== "ok") {
1024
+ console.log(` Guardrail: ${runtimeGuard.detail}`);
1025
+ }
948
1026
  if (configuredProgramArguments !== null && configuredProgramArguments.length > 0) {
949
1027
  console.log(` Program: ${configuredProgramArguments[0]}`);
950
1028
  if (configuredProgramArguments.length > 1) {
@@ -1,3 +1,5 @@
1
+ const LEGACY_COMPAT_PACKAGE_NAME = "@jonathangu/openclawbrain";
2
+
1
3
  const CHANGE_REASON_LABELS = {
2
4
  install_identity: "plugin install identity changed",
3
5
  install_layout: "authoritative install layout changed",
@@ -26,6 +28,14 @@ function installIdentityOf(fingerprint) {
26
28
  });
27
29
  }
28
30
 
31
+ export function shouldReplaceOpenClawBrainInstallBeforeConverge(fingerprint) {
32
+ const selectedInstall = fingerprint?.selectedInstall ?? null;
33
+ if (selectedInstall === null) {
34
+ return false;
35
+ }
36
+ return selectedInstall.packageName !== "@openclawbrain/openclaw";
37
+ }
38
+
29
39
  export function planOpenClawBrainConvergePluginAction(fingerprint) {
30
40
  const selectedInstall = fingerprint?.selectedInstall ?? null;
31
41
  if (selectedInstall === null) {
@@ -36,6 +46,14 @@ export function planOpenClawBrainConvergePluginAction(fingerprint) {
36
46
  reason: "no authoritative OpenClawBrain plugin install was discovered for this OpenClaw home",
37
47
  };
38
48
  }
49
+ if (selectedInstall.packageName === LEGACY_COMPAT_PACKAGE_NAME) {
50
+ return {
51
+ action: "install",
52
+ packageSpec: "@openclawbrain/openclaw",
53
+ pluginId: "openclawbrain",
54
+ reason: `the authoritative install still points at the retired compatibility package ${LEGACY_COMPAT_PACKAGE_NAME}, so converge must replace it with the canonical split-package plugin`,
55
+ };
56
+ }
39
57
  if (selectedInstall.installLayout !== "native_package_plugin") {
40
58
  return {
41
59
  action: "install",
@@ -130,6 +148,10 @@ export function classifyOpenClawBrainConvergeVerification(input) {
130
148
  const runtimeLoad = input.runtimeLoad ?? "unverified";
131
149
  const loadProof = input.loadProof ?? "unverified";
132
150
  const serveState = input.serveState ?? "unknown";
151
+ const installedPackageName = input.installedPackageName ?? null;
152
+ if (installedPackageName === LEGACY_COMPAT_PACKAGE_NAME) {
153
+ blockingReasons.push(`installed plugin still references the retired compatibility package ${LEGACY_COMPAT_PACKAGE_NAME}; converge must replace it with @openclawbrain/openclaw`);
154
+ }
133
155
  if (installLayout !== "native_package_plugin") {
134
156
  blockingReasons.push("split-package native package plugin is not authoritative after converge");
135
157
  }
@@ -160,6 +182,23 @@ export function classifyOpenClawBrainConvergeVerification(input) {
160
182
  if (input.restartRequired === true && input.restartPerformed !== true && runtimeTruthAlreadyProven) {
161
183
  warnings.push("automatic restart was not performed because install could not infer an exact OpenClaw profile token, but current status already proves runtime load");
162
184
  }
185
+ // When runtime proof is green for the repaired profile, promote the proof
186
+ // surface to healthy rather than leaving stale degraded warnings that
187
+ // contradict the proven truth.
188
+ if (runtimeTruthAlreadyProven) {
189
+ if (serveState !== "serving_active_pack") {
190
+ warnings.push(`serve state is ${serveState}`);
191
+ }
192
+ if (input.awaitingFirstExport === true) {
193
+ warnings.push("the attached profile has not emitted its first export yet");
194
+ }
195
+ return {
196
+ state: warnings.length > 0 ? "warning" : "healthy",
197
+ manualActionRequired: false,
198
+ blockingReasons,
199
+ warnings,
200
+ };
201
+ }
163
202
  if (displayedStatus !== "ok") {
164
203
  warnings.push(`status is ${displayedStatus}`);
165
204
  }
@@ -384,20 +384,33 @@ function buildHardeningSnapshot({ attachTruthLine, serveLine, routeFnLine, verdi
384
384
  };
385
385
  }
386
386
 
387
- function hasPackagedHookSource(pluginInspectText) {
388
- return /Source:\s+.*(?:@openclawbrain[\\/]+openclaw|openclawbrain)[\\/]+dist[\\/]+extension[\\/]+index\.js/m.test(pluginInspectText);
387
+ function hasPackagedHookSource(pluginInspectText, openclawHome) {
388
+ if (/Source:\s+.*(?:@openclawbrain[\\/]+openclaw|openclawbrain)[\\/]+dist[\\/]+extension[\\/]+index\.js/m.test(pluginInspectText)) {
389
+ return true;
390
+ }
391
+ const generatedShadowHookPath = canonicalizeExistingProofPath(path.join(openclawHome, "extensions", "openclawbrain", "index.ts"));
392
+ const sourceMatch = pluginInspectText.match(/^Source:\s+(.+)$/m);
393
+ const reportedSourcePath = canonicalizeExistingProofPath(sourceMatch?.[1] ?? "");
394
+ return reportedSourcePath === generatedShadowHookPath;
389
395
  }
390
396
 
391
- function buildVerdict({ steps, gatewayStatus, pluginInspect, statusSignals, breadcrumbs, runtimeLoadProofSnapshot, openclawHome }) {
397
+ function buildVerdict({ steps, gatewayStatus, pluginInspect, statusSignals, breadcrumbs, runtimeLoadProofSnapshot, coverageSnapshot, openclawHome }) {
392
398
  const failedSteps = steps.filter((step) => step.resultClass !== "success" && step.skipped !== true);
393
399
  const failedDetailedStatusStep = failedSteps.find((step) => step.stepId === "05-detailed-status");
394
400
  const gatewayHealthy = /Runtime:\s+running/m.test(gatewayStatus) && /RPC probe:\s+ok/m.test(gatewayStatus);
395
401
  const pluginLoaded = /Status:\s+loaded/m.test(pluginInspect);
396
- const packagedHookPath = hasPackagedHookSource(pluginInspect);
402
+ const packagedHookPath = hasPackagedHookSource(pluginInspect, openclawHome);
397
403
  const breadcrumbLoaded = breadcrumbs.afterBundleStart.some((entry) => entry.kind === "loaded");
398
404
  const runtimeProofMatched = Array.isArray(runtimeLoadProofSnapshot?.value?.profiles)
399
405
  && runtimeLoadProofSnapshot.value.profiles.some((profile) => canonicalizeExistingProofPath(profile?.openclawHome ?? "") === canonicalizeExistingProofPath(openclawHome));
400
406
  const runtimeTruthGaps = [];
407
+ const currentCoverageEntry = Array.isArray(coverageSnapshot?.profiles)
408
+ ? coverageSnapshot.profiles.find((entry) => entry.current)
409
+ : null;
410
+ const currentProfileRuntimeCovered = currentCoverageEntry?.runtimeLoad === "proven";
411
+ const crossProfileCoverageGapOnly = currentProfileRuntimeCovered
412
+ && (coverageSnapshot?.attachedProfileCount ?? 0) > (coverageSnapshot?.runtimeProvenCount ?? 0)
413
+ && (statusSignals.proofError === null || statusSignals.proofError === "none");
401
414
  const strongRuntimeTruth = statusSignals.loadProofReady
402
415
  && statusSignals.runtimeProven
403
416
  && statusSignals.serveActivePack
@@ -414,8 +427,10 @@ function buildVerdict({ steps, gatewayStatus, pluginInspect, statusSignals, brea
414
427
  const warnings = [];
415
428
  if (!statusSignals.statusOk) {
416
429
  if (strongRuntimeTruth) {
417
- warningCodes.push("status_warn");
418
- warnings.push("detailed status did not return STATUS ok, but loadProof/runtime/serve/routeFn proofs stayed healthy");
430
+ if (!crossProfileCoverageGapOnly) {
431
+ warningCodes.push("status_warn");
432
+ warnings.push("detailed status did not return STATUS ok, but loadProof/runtime/serve/routeFn proofs stayed healthy");
433
+ }
419
434
  }
420
435
  else {
421
436
  runtimeTruthGaps.push("status_ok");
@@ -870,6 +885,7 @@ export function captureOperatorProofBundle(options) {
870
885
  statusSignals,
871
886
  breadcrumbs,
872
887
  runtimeLoadProofSnapshot,
888
+ coverageSnapshot,
873
889
  openclawHome: options.openclawHome,
874
890
  });
875
891
  const hardeningSnapshot = buildHardeningSnapshot({
@@ -2,7 +2,7 @@
2
2
  * OpenClawBrain extension template — canonical, pre-built, versioned with the package.
3
3
  *
4
4
  * The placeholder __ACTIVATION_ROOT__ is replaced by
5
- * `@openclawbrain/cli`'s `openclawbrain install`
5
+ * OpenClawBrain's `openclawbrain install`
6
6
  * with the real activation root path at install time.
7
7
  *
8
8
  * Design constraints:
@@ -91,7 +91,7 @@ function announceStartupBreadcrumb(): void {
91
91
  if (isActivationRootPlaceholder(ACTIVATION_ROOT)) {
92
92
  warnOnce(
93
93
  "startup-brain-not-yet-loaded",
94
- "[openclawbrain] BRAIN NOT YET LOADED: install has not pinned ACTIVATION_ROOT yet. Install @openclawbrain/cli, then run: openclawbrain install --openclaw-home <path>"
94
+ "[openclawbrain] BRAIN NOT YET LOADED: install has not pinned ACTIVATION_ROOT yet. Install OpenClawBrain, then run: openclawbrain install --openclaw-home <path>"
95
95
  );
96
96
  return;
97
97
  }
@@ -162,7 +162,7 @@ export function createBeforePromptBuildHandler(input: {
162
162
  key: "activation-root-placeholder",
163
163
  once: true,
164
164
  message:
165
- "[openclawbrain] BRAIN NOT YET LOADED: ACTIVATION_ROOT is still a placeholder. Install @openclawbrain/cli, then run: openclawbrain install --openclaw-home <path>"
165
+ "[openclawbrain] BRAIN NOT YET LOADED: ACTIVATION_ROOT is still a placeholder. Install OpenClawBrain, then run: openclawbrain install --openclaw-home <path>"
166
166
  }));
167
167
  return {};
168
168
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclawbrain/cli",
3
- "version": "0.4.20",
3
+ "version": "0.4.21",
4
4
  "description": "OpenClawBrain operator CLI package with install/status helpers, daemon controls, and import/export tooling.",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",