@openclawbrain/openclaw 0.3.4 → 0.3.6

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
@@ -7,9 +7,62 @@ This is the default front door for first-time OpenClawBrain users.
7
7
  - Start here when you want one package to import from instead of guessing across the `@openclawbrain/*` split.
8
8
  - The stable front-door names are `@openclawbrain/openclaw` and `openclawbrain`.
9
9
  - If you are validating this repo-tip checkout, prepare the checkout with `pnpm install --frozen-lockfile`, run `pnpm release:status`, then `pnpm release:pack`, then install the local `.release/*.tgz` tarballs.
10
- - If you are intentionally consuming a later tagged and published wave, install it with `npm install @openclawbrain/openclaw`.
10
+ - For the current published wave, install it with `npm install -g @openclawbrain/openclaw@0.3.5`.
11
11
  - Use the `openclawbrain` CLI for status, rollback, and narrow scanner scans; `openclawbrain-ops` stays available as a compatibility alias.
12
12
  - `npm exec openclawbrain -- --help` works after either install path, but the exact quickstart commands remain the clearest first attach path.
13
+ - OpenClaw can also install this same package as a native plugin package; keep the `openclawbrain` CLI available and run `openclawbrain install --openclaw-home <path>` afterward to pin the activation root for that installed copy.
14
+
15
+ The compatibility package `@jonathangu/openclawbrain@0.3.5` remains published for older plugin/wrapper installs, but it is not the main operator path.
16
+
17
+ Decision and migration note: [`docs/lifecycle.md`](../../docs/lifecycle.md)
18
+
19
+ ## Operator quickstart
20
+
21
+ Install or upgrade through the same lane:
22
+
23
+ ```bash
24
+ npm install -g @openclawbrain/openclaw@0.3.5
25
+ openclawbrain install --openclaw-home ~/.openclaw
26
+ openclaw gateway restart
27
+ openclawbrain status --openclaw-home ~/.openclaw --detailed
28
+ ```
29
+
30
+ If you want OpenClaw to own the plugin package directory itself, the native package lane is:
31
+
32
+ ```bash
33
+ openclaw plugins install @openclawbrain/openclaw@0.3.5
34
+ openclawbrain install --openclaw-home ~/.openclaw
35
+ openclaw gateway restart
36
+ openclawbrain status --openclaw-home ~/.openclaw --detailed
37
+ ```
38
+
39
+ The CLI still pins the activation root for either layout. `status --openclaw-home` now tells the truth about whether the loaded hook is the generated shadow extension or the native package plugin.
40
+
41
+ Verify the installed target:
42
+
43
+ ```bash
44
+ openclawbrain status --openclaw-home ~/.openclaw --detailed
45
+ openclawbrain status --openclaw-home ~/.openclaw --json
46
+ ```
47
+
48
+ Remove only the OpenClaw profile hook and keep brain data:
49
+
50
+ ```bash
51
+ openclawbrain detach --openclaw-home ~/.openclaw
52
+ openclaw gateway restart
53
+ ```
54
+
55
+ Remove the hook and purge the OpenClawBrain data for that install:
56
+
57
+ ```bash
58
+ openclawbrain uninstall --openclaw-home ~/.openclaw --purge-data
59
+ openclaw gateway restart
60
+ npm uninstall -g @openclawbrain/openclaw
61
+ ```
62
+
63
+ If you want explicit uninstall semantics without purging, use `openclawbrain uninstall --openclaw-home ~/.openclaw --keep-data`. `detach` remains the simpler keep-data path.
64
+
65
+ `detach` and `uninstall` remove whichever OpenClawBrain hook layout is installed for that OpenClaw home. The data outcome is still explicit: `detach` keeps data, `uninstall --keep-data` keeps it explicitly, and `uninstall --purge-data` deletes it.
13
66
 
14
67
  Use this package when OpenClaw needs the narrow supported operator bridge captured by `OPERATOR_API_CONTRACT_V1`:
15
68
 
@@ -22,7 +75,7 @@ Use this package when OpenClaw needs the narrow supported operator bridge captur
22
75
 
23
76
  The supported operator contract is intentionally decomposed into bootstrap/attach, status, export, refresh, promote, rollback, and proof/observability. The post-attach loop stays off the hot path: it exports turns, builds candidate packs, and promotes them later rather than mutating the current active pack in place.
24
77
 
25
- Read `docs/operator-api-contract.md` for the family-by-family contract map and the explicitly quarantined surfaces.
78
+ Use `docs/lifecycle.md` for the copy-paste install/upgrade/verify/remove story. The rest of this README is the family-by-family contract map and the explicitly quarantined surfaces.
26
79
 
27
80
  ## Operator boundary
28
81
 
@@ -101,12 +154,12 @@ This package stays fail-open for non-learned-required compile misses, and event-
101
154
 
102
155
  Keep `install` as the front-door lifecycle command:
103
156
 
104
- - `openclawbrain install` is the default attach path. On many-profile hosts, pass `--openclaw-home <path>` or set `OPENCLAW_HOME` so the target profile is explicit instead of guessed.
157
+ - `openclawbrain install --openclaw-home <path>` is the default attach path. On many-profile hosts, pass `--openclaw-home <path>` or set `OPENCLAW_HOME` so the target profile is explicit instead of guessed.
105
158
  - `openclawbrain install` now writes only the real profile hook and activation state. It no longer creates `BRAIN.md` or rewrites `AGENTS.md`.
106
159
  - `openclawbrain detach --openclaw-home <path>` removes only the OpenClaw profile hook. It preserves OpenClawBrain activation data and now says so plainly in both human output and JSON.
107
160
  - `openclawbrain uninstall --openclaw-home <path> --keep-data|--purge-data` removes the profile hook and forces the operator to pick the data outcome explicitly.
108
161
  - `--restart <never|safe|external>` is guidance only for detach/uninstall. The default `safe` guidance stays conservative: restart the running OpenClaw profile before expecting hook-state changes to take effect; otherwise the next launch picks them up.
109
- - plain `status` stays the human answer to “How’s the brain?” and now keeps compact lifecycle truth visible without dumping teacher/no-op or other proof chatter by default; use `--detailed` when you want the dense operator report.
162
+ - plain `status` stays the human answer to “How’s the brain?” and now keeps compact lifecycle truth visible without dumping teacher/no-op or other proof chatter by default; use `--openclaw-home <path> --detailed` when you want the dense operator report for one installed target.
110
163
 
111
164
  ## Narrow operator contract
112
165
 
@@ -35,13 +35,16 @@ export function normalizePromptBuildEvent(event) {
35
35
  const warnings = [];
36
36
  const sessionId = normalizeOptionalScalarField(event.sessionId, "sessionId", warnings);
37
37
  const channel = normalizeOptionalScalarField(event.channel, "channel", warnings);
38
- let extractedMessage = "";
38
+ const promptFallback = extractTextContent(event.prompt);
39
+ let extractedMessage = promptFallback ?? "";
39
40
  if (messages.length === 0) {
40
- warnings.push(failOpenDiagnostic("runtime-messages-empty", "before_prompt_build event.messages is empty", `event=${describeValue(event)}`));
41
+ if (extractedMessage.length === 0) {
42
+ warnings.push(failOpenDiagnostic("runtime-messages-empty", "before_prompt_build event.messages is empty", `event=${describeValue(event)}`));
43
+ }
41
44
  }
42
45
  else {
43
46
  const lastMessage = messages.at(-1);
44
- extractedMessage = extractPromptMessage(lastMessage) ?? "";
47
+ extractedMessage = extractPromptMessage(lastMessage) ?? promptFallback ?? "";
45
48
  if (extractedMessage.length === 0) {
46
49
  warnings.push(failOpenDiagnostic("runtime-last-message-invalid", "before_prompt_build last message has no usable text content", `lastMessage=${describeValue(lastMessage)}`));
47
50
  }
@@ -1,6 +1,7 @@
1
1
  import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { inspectOpenClawHome } from "./openclaw-home-layout.js";
4
+ import { resolveOpenClawHomeFromExtensionEntryPath } from "./openclaw-plugin-install.js";
4
5
  const ATTACHMENT_RUNTIME_LOAD_PROOFS_CONTRACT = "openclaw_profile_runtime_load_proofs.v1";
5
6
  const ATTACHMENT_TRUTH_DIRNAME = "attachment-truth";
6
7
  const ATTACHMENT_RUNTIME_LOAD_PROOFS_BASENAME = "runtime-load-proofs.json";
@@ -128,7 +129,11 @@ function writeRuntimeLoadProofs(proofPath, proofs) {
128
129
  writeFileSync(proofPath, `${JSON.stringify(proofs, null, 2)}\n`, "utf8");
129
130
  }
130
131
  function deriveOpenClawHomeFromExtensionEntryPath(extensionEntryPath) {
131
- return canonicalizeFilesystemPath(path.resolve(path.dirname(canonicalizeFilesystemPath(extensionEntryPath)), "..", ".."));
132
+ const openclawHome = resolveOpenClawHomeFromExtensionEntryPath(extensionEntryPath);
133
+ if (openclawHome === null) {
134
+ throw new Error(`extension entry path ${extensionEntryPath} is not nested under an OpenClaw extensions dir`);
135
+ }
136
+ return canonicalizeFilesystemPath(openclawHome);
132
137
  }
133
138
  export function resolveAttachmentRuntimeLoadProofsPath(activationRoot) {
134
139
  return path.join(path.resolve(activationRoot), ATTACHMENT_TRUTH_DIRNAME, ATTACHMENT_RUNTIME_LOAD_PROOFS_BASENAME);
@@ -212,4 +217,4 @@ export function clearOpenClawProfileRuntimeLoadProof(input) {
212
217
  });
213
218
  return true;
214
219
  }
215
- //# sourceMappingURL=attachment-truth.js.map
220
+ //# sourceMappingURL=attachment-truth.js.map
package/dist/src/cli.js CHANGED
@@ -14,6 +14,7 @@ import { inspectActivationState, loadPackFromActivation, promoteCandidatePack, r
14
14
  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
+ import { describeOpenClawBrainInstallIdentity, describeOpenClawBrainInstallLayout, findInstalledOpenClawBrainPlugin, getOpenClawBrainKnownPluginIds, pinInstalledOpenClawBrainPluginActivationRoot, resolveOpenClawBrainInstallTarget } from "./openclaw-plugin-install.js";
17
18
  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, writeScannedEventExportBundle } from "./index.js";
18
19
  import { appendLearningUpdateLogs } from "./learning-spine.js";
19
20
  import { buildPassiveLearningSessionExportFromOpenClawSessionStore } from "./local-session-passive-learning.js";
@@ -64,7 +65,9 @@ function discoverInstallCandidateOpenClawHomes(homeDir = getCliHomeDir()) {
64
65
  function summarizeSharedActivationRootReferenceProof(reference) {
65
66
  switch (reference.installState) {
66
67
  case "installed":
67
- return "hook installed and loadable";
68
+ return reference.serveAttachmentState === "attached"
69
+ ? "hook installed and loadable"
70
+ : "hook files exist, but the current install is not loadable yet";
68
71
  case "blocked_by_allowlist":
69
72
  return "hook files exist but OpenClaw will not load them because plugins.allow blocks openclawbrain";
70
73
  case "not_installed":
@@ -122,7 +125,9 @@ function findInstalledHookReferencesForActivationRoot(input) {
122
125
  installState: installHook.state === "installed" || installHook.state === "blocked_by_allowlist"
123
126
  ? installHook.state
124
127
  : "not_installed",
125
- serveAttachmentState: installHook.state === "installed" ? "attached" : "half_attached",
128
+ serveAttachmentState: installHook.state === "installed" && installHook.loadability === "loadable"
129
+ ? "attached"
130
+ : "half_attached",
126
131
  hookDetail: installHook.detail
127
132
  };
128
133
  return path.resolve(installedActivationRoot) === resolvedActivationRoot
@@ -503,7 +508,7 @@ function operatorCliHelp() {
503
508
  " --help Show this help.",
504
509
  "",
505
510
  "Lifecycle flow:",
506
- " 1. install openclawbrain install — safe first-time default; pass --openclaw-home when more than one OpenClaw home/layout is present",
511
+ " 1. install openclawbrain install — safe first-time default; writes the generated shadow hook or pins an already-installed native package plugin for one OpenClaw home",
507
512
  " 2. attach openclawbrain attach --openclaw-home <path> [--activation-root <path>] — explicit reattach/manual hook path for known brain data; use install first",
508
513
  " 3. status openclawbrain status --activation-root <path> — answer \"How's the brain?\" for that boundary",
509
514
  " 4. status --detailed openclawbrain status --activation-root <path> --detailed — explain serve path, freshness, backlog, and failure mode",
@@ -519,6 +524,7 @@ function operatorCliHelp() {
519
524
  " scan inspect one recorded session or live event export without claiming a daemon is running",
520
525
  " learn one-shot local-session learning pass against the resolved activation root",
521
526
  " status --teacher-snapshot keeps the current live-first / principal-priority / passive-backfill learner order visible when that snapshot exists",
527
+ " native package installs still need the openclawbrain CLI available because install/attach pin the activation root for that package copy",
522
528
  " watch/daemon persist their operator snapshot at <activation-root>/watch/teacher-snapshot.json; --teacher-snapshot overrides the default path",
523
529
  " watch teacher defaults come from install-written provider-defaults.json under the activation root; OPENCLAWBRAIN_TEACHER_* and OPENCLAWBRAIN_EMBEDDER_* are host-shell overrides only, not live gateway wiring",
524
530
  "",
@@ -678,12 +684,16 @@ function summarizeStatusInstallHook(openclawHome) {
678
684
  const hook = inspectOpenClawBrainHookStatus(openclawHome);
679
685
  return {
680
686
  state: hook.installState === "unverified" ? "unknown" : hook.installState,
687
+ loadability: hook.loadability,
688
+ installLayout: hook.installLayout,
689
+ hookPath: hook.hookPath,
681
690
  detail: hook.detail
682
691
  };
683
692
  }
684
693
  function summarizeStatusHookLoad(installHook, status) {
685
694
  return {
686
695
  installState: installHook.state === "unknown" ? "unverified" : installHook.state,
696
+ loadability: installHook.loadability,
687
697
  loadProof: status.hook.loadProof,
688
698
  detail: status.hook.detail
689
699
  };
@@ -1243,7 +1253,9 @@ function formatAttachedProfileTruthDetailedList(entries) {
1243
1253
  .join(" ");
1244
1254
  }
1245
1255
  function summarizeDisplayedStatus(status, installHook) {
1246
- return installHook.state === "blocked_by_allowlist" ? "fail" : status.brainStatus.status;
1256
+ return installHook.state === "blocked_by_allowlist" || status.hook.loadability === "blocked"
1257
+ ? "fail"
1258
+ : status.brainStatus.status;
1247
1259
  }
1248
1260
  function buildCompactStatusHeader(status, report, options) {
1249
1261
  const installHook = summarizeStatusInstallHook(options.openclawHome);
@@ -1262,7 +1274,7 @@ function buildCompactStatusHeader(status, report, options) {
1262
1274
  });
1263
1275
  return [
1264
1276
  `lifecycle attach=${status.attachment.state} learner=${yesNo(status.passiveLearning.learnerRunning)} watch=${summarizeStatusWatchState(status)} export=${status.passiveLearning.exportState} promote=${summarizeStatusPromotionState(status)} serve=${summarizeStatusServeReality(status)}`,
1265
- `hook install=${hookLoad.installState} loadProof=${hookLoad.loadProof} detail=${hookLoad.detail}`,
1277
+ `hook install=${hookLoad.installState} loadability=${hookLoad.loadability} loadProof=${hookLoad.loadProof} layout=${status.hook.installLayout ?? "unverified"} additional=${status.hook.additionalInstallCount ?? 0} detail=${hookLoad.detail}`,
1266
1278
  `attachTruth current=${attachmentTruth.currentProfileLabel} hook=${attachmentTruth.hookFiles} config=${attachmentTruth.configLoad} runtime=${attachmentTruth.runtimeLoad} watcher=${attachmentTruth.watcher} attachedSet=${formatAttachedProfileTruthCompactList(attachmentTruth.attachedProfiles)} why=${attachmentTruth.detail}`,
1267
1279
  `passive firstExport=${yesNo(status.passiveLearning.firstExportOccurred)} backlog=${status.passiveLearning.backlogState} pending=${formatStatusNullableNumber(status.passiveLearning.pendingLive)}/${formatStatusNullableNumber(status.passiveLearning.pendingBackfill)}`,
1268
1280
  `serving pack=${status.passiveLearning.currentServingPackId ?? "none"} lastExport=${status.passiveLearning.lastExportAt ?? "none"} lastPromotion=${status.passiveLearning.lastPromotionAt ?? "none"}`,
@@ -1293,7 +1305,7 @@ function formatCurrentProfileStatusSummary(status, report, targetInspection, opt
1293
1305
  const profileIdSuffix = status.profile.profileId === null ? "" : ` id=${status.profile.profileId}`;
1294
1306
  const targetLine = targetInspection === null
1295
1307
  ? `target activation=${status.host.activationRoot} source=activation_root_only`
1296
- : `target activation=${status.host.activationRoot} ${formatOpenClawTargetLine(targetInspection)} hook=${shortenPath(path.join(targetInspection.openclawHome, "extensions", "openclawbrain", "index.ts"))}`;
1308
+ : `target activation=${status.host.activationRoot} ${formatOpenClawTargetLine(targetInspection)} hook=${status.hook.hookPath === null ? "unverified" : shortenPath(status.hook.hookPath)}`;
1297
1309
  return [
1298
1310
  `STATUS ${displayedStatus}`,
1299
1311
  ...buildCompactStatusHeader(status, report, options),
@@ -1555,6 +1567,7 @@ function shouldWriteProfileHookProviderDefaults(parsed, activationPlan, isInstal
1555
1567
  }
1556
1568
  function buildInstallBrainFeedbackSummary(input) {
1557
1569
  const providerDefaultsPath = resolveOpenClawBrainProviderDefaultsPath(input.parsed.activationRoot);
1570
+ const hookLayout = describeOpenClawBrainInstallLayout(input.hookLayout);
1558
1571
  const embedderState = input.embedderProvision === null ? "unchanged" : input.embedderProvision.state;
1559
1572
  const teacherDefaults = input.providerDefaults?.defaults.teacher;
1560
1573
  const teacherProvider = teacherDefaults?.provider ?? "unknown";
@@ -1598,15 +1611,16 @@ function buildInstallBrainFeedbackSummary(input) {
1598
1611
  gatewayStatusCommand: buildGatewayStatusCommand(profileName)
1599
1612
  };
1600
1613
  const provedNow = input.activationPlan.action === "bootstrap"
1601
- ? `hook written, activation root ready, seed/current-profile attach bootstrapped, learner service ${input.learnerService.state}, provider defaults ${input.providerDefaults === null ? "kept" : "written"}`
1602
- : `hook written, activation root kept, active pack ${input.activationPlan.activePackId ?? "unknown"} preserved, learner service ${input.learnerService.state}${input.providerDefaults === null ? "" : ", provider defaults written"}`;
1614
+ ? `${hookLayout} prepared, activation root ready, seed/current-profile attach bootstrapped, learner service ${input.learnerService.state}, provider defaults ${input.providerDefaults === null ? "kept" : "written"}`
1615
+ : `${hookLayout} prepared, activation root kept, active pack ${input.activationPlan.activePackId ?? "unknown"} preserved, learner service ${input.learnerService.state}${input.providerDefaults === null ? "" : ", provider defaults written"}`;
1603
1616
  const notYetProved = input.learnerService.state === "deferred"
1604
1617
  ? `OpenClaw has not reloaded this hook yet, and passive learner auto-start was deferred; restart plus status still must prove serve-path load, while learner-service start remains a separate operator check`
1605
1618
  : input.activationPlan.action === "bootstrap"
1606
1619
  ? `Passive learning is wired for this activation root, but OpenClaw has not reloaded the hook yet; restart plus status still must prove live startup/load and the first exported turn`
1607
1620
  : `Passive learning is wired for this activation root, but this ${input.parsed.command} run does not itself prove live startup/load after restart`;
1608
1621
  return {
1609
- hookPath: input.extensionDir,
1622
+ hookPath: input.hookPath,
1623
+ hookLayout: input.hookLayout,
1610
1624
  providerDefaultsPath,
1611
1625
  profile: {
1612
1626
  exactProfileName: profileName,
@@ -1644,7 +1658,7 @@ function buildInstallBrainFeedbackSummary(input) {
1644
1658
  profileName === null
1645
1659
  ? "profile exactName=unresolved selector=current_profile casing=not_available"
1646
1660
  : `profile exactName=${quoteShellArg(profileName)} source=${profileSource} casing=preserved`,
1647
- `hook written=${shortenPath(input.extensionDir)}`,
1661
+ `hook layout=${input.hookLayout} path=${shortenPath(input.hookPath)}`,
1648
1662
  `activation root=${shortenPath(input.parsed.activationRoot)} source=${formatInstallActivationRootSource(input.parsed.activationRootSource)}`,
1649
1663
  `attachment policy=${attachment.policy} rootMode=${attachment.activationRootMode} sameGatewayProof=${attachment.sameGatewayProof} detail=${attachment.detail}`,
1650
1664
  `defaults provider-defaults=${shortenPath(providerDefaultsPath)} state=${input.providerDefaults === null ? "unchanged" : "written"}`,
@@ -1761,6 +1775,14 @@ function buildStatusNextStep(status, report, options) {
1761
1775
  `(rerun ${buildInstallCommand(options.openclawHome)} or ${buildAttachCommand(options.openclawHome, status.host.activationRoot)}) ` +
1762
1776
  "before trusting serve-path status again.");
1763
1777
  }
1778
+ if (status.hook.loadability === "blocked") {
1779
+ if (options.openclawHome === null) {
1780
+ return "Repair the installed hook so it pins a real activation root before trusting serve-path status again.";
1781
+ }
1782
+ return (`Repair the installed ${status.hook.installLayout === "native_package_plugin" ? "native package plugin" : "profile hook"} ` +
1783
+ `(rerun ${buildInstallCommand(options.openclawHome)} or ${buildAttachCommand(options.openclawHome, status.host.activationRoot)}) ` +
1784
+ "before trusting serve-path status again.");
1785
+ }
1764
1786
  if (status.brainStatus.activationState === "broken_install") {
1765
1787
  return "Repair or replace the activation root before trusting serve-path status again.";
1766
1788
  }
@@ -3142,13 +3164,17 @@ function runProfileHookAttachCommand(parsed) {
3142
3164
  const commandLabel = parsed.command.toUpperCase();
3143
3165
  const isInstall = parsed.command === "install";
3144
3166
  const targetInspection = inspectOpenClawHome(parsed.openclawHome);
3145
- const extensionDir = path.join(parsed.openclawHome, "extensions", "openclawbrain");
3167
+ const installTarget = resolveOpenClawBrainInstallTarget(parsed.openclawHome);
3168
+ const extensionDir = installTarget.extensionDir;
3146
3169
  steps.push(`Target OpenClaw home: ${parsed.openclawHome} (${formatInstallOpenClawHomeSource(parsed.openclawHomeSource)})`);
3147
3170
  steps.push(isInstall
3148
3171
  ? "Lifecycle mode: install is the safe first-time default for wiring one profile to one activation root."
3149
3172
  : "Lifecycle mode: attach is the explicit reattach/manual profile-hook path; use install for first-time setup.");
3150
3173
  steps.push(`Detected layout: ${formatOpenClawTargetExplanation(targetInspection)}`);
3151
- steps.push(`Target hook path: ${extensionDir}`);
3174
+ steps.push(`Target hook path: ${installTarget.hookPath} (${describeOpenClawBrainInstallLayout(installTarget.installLayout)})`);
3175
+ if (installTarget.selectedInstall !== null && installTarget.additionalInstalls.length > 0) {
3176
+ steps.push(`Kept ${describeOpenClawBrainInstallLayout(installTarget.selectedInstall.installLayout)} authoritative at ${shortenPath(installTarget.selectedInstall.extensionDir)} (${describeOpenClawBrainInstallIdentity(installTarget.selectedInstall)}); additional installs remain at ${installTarget.additionalInstalls.map((install) => `${shortenPath(install.extensionDir)} (${describeOpenClawBrainInstallLayout(install.installLayout)})`).join(", ")}`);
3177
+ }
3152
3178
  // 1. Validate --openclaw-home exists and has openclaw.json
3153
3179
  validateOpenClawHome(parsed.openclawHome);
3154
3180
  // 2. Inspect the activation root before writing profile hook artifacts.
@@ -3210,59 +3236,60 @@ function runProfileHookAttachCommand(parsed) {
3210
3236
  ? `Kept inspected activation state: active pack ${activationPlan.activePackId}`
3211
3237
  : `Reused inspected activation state for explicit attach: active pack ${activationPlan.activePackId}`);
3212
3238
  }
3213
- // 7-10. Write extension files
3214
- mkdirSync(extensionDir, { recursive: true });
3215
- // 5. Write index.ts
3216
- const indexTsPath = path.join(extensionDir, "index.ts");
3217
- writeFileSync(indexTsPath, buildExtensionIndexTs(parsed.activationRoot), "utf8");
3218
- steps.push(`Wrote extension: ${indexTsPath}`);
3219
- // 5b. Write runtime-guard files (imported by index.ts as ./runtime-guard.js)
3220
- const runtimeGuardPaths = resolveExtensionRuntimeGuardPath();
3221
- if (runtimeGuardPaths.ts !== null) {
3222
- const runtimeGuardTsPath = path.join(extensionDir, "runtime-guard.ts");
3223
- writeFileSync(runtimeGuardTsPath, readFileSync(runtimeGuardPaths.ts, "utf8"), "utf8");
3224
- steps.push(`Wrote extension runtime-guard source: ${runtimeGuardTsPath}`);
3225
- }
3226
- const runtimeGuardJsPath = path.join(extensionDir, "runtime-guard.js");
3227
- writeFileSync(runtimeGuardJsPath, readFileSync(runtimeGuardPaths.js, "utf8"), "utf8");
3228
- steps.push(`Wrote extension runtime-guard: ${runtimeGuardJsPath}`);
3229
- // 6. Write package.json
3230
- const packageJsonPath = path.join(extensionDir, "package.json");
3231
- writeFileSync(packageJsonPath, buildExtensionPackageJson(), "utf8");
3232
- steps.push(`Wrote package.json: ${packageJsonPath}`);
3233
- // 7. npm install
3234
- const releaseTarballInstall = resolveExtensionInstallReleaseTarballs();
3235
- try {
3236
- if (releaseTarballInstall !== null) {
3237
- execFileSync(resolveNpmCommand(), ["install", "--ignore-scripts", "--no-save", ...releaseTarballInstall.tarballs], { cwd: extensionDir, stdio: "pipe" });
3238
- steps.push(`Installed extension dependencies from release artifacts: ${releaseTarballInstall.tarballs.length} tarballs from ${releaseTarballInstall.artifactDir}`);
3239
+ // 7-10. Prepare the hook layout that OpenClaw will actually load.
3240
+ if (installTarget.writeMode === "pin_native_package") {
3241
+ pinInstalledOpenClawBrainPluginActivationRoot(installTarget.hookPath, parsed.activationRoot);
3242
+ steps.push(`Pinned native package plugin loader: ${installTarget.hookPath}`);
3243
+ }
3244
+ else {
3245
+ mkdirSync(extensionDir, { recursive: true });
3246
+ const indexTsPath = path.join(extensionDir, "index.ts");
3247
+ writeFileSync(indexTsPath, buildExtensionIndexTs(parsed.activationRoot), "utf8");
3248
+ steps.push(`Wrote extension: ${indexTsPath}`);
3249
+ const runtimeGuardPaths = resolveExtensionRuntimeGuardPath();
3250
+ if (runtimeGuardPaths.ts !== null) {
3251
+ const runtimeGuardTsPath = path.join(extensionDir, "runtime-guard.ts");
3252
+ writeFileSync(runtimeGuardTsPath, readFileSync(runtimeGuardPaths.ts, "utf8"), "utf8");
3253
+ steps.push(`Wrote extension runtime-guard source: ${runtimeGuardTsPath}`);
3254
+ }
3255
+ const runtimeGuardJsPath = path.join(extensionDir, "runtime-guard.js");
3256
+ writeFileSync(runtimeGuardJsPath, readFileSync(runtimeGuardPaths.js, "utf8"), "utf8");
3257
+ steps.push(`Wrote extension runtime-guard: ${runtimeGuardJsPath}`);
3258
+ const packageJsonPath = path.join(extensionDir, "package.json");
3259
+ writeFileSync(packageJsonPath, buildExtensionPackageJson(), "utf8");
3260
+ steps.push(`Wrote package.json: ${packageJsonPath}`);
3261
+ const releaseTarballInstall = resolveExtensionInstallReleaseTarballs();
3262
+ try {
3263
+ if (releaseTarballInstall !== null) {
3264
+ execFileSync(resolveNpmCommand(), ["install", "--ignore-scripts", "--no-save", ...releaseTarballInstall.tarballs], { cwd: extensionDir, stdio: "pipe" });
3265
+ steps.push(`Installed extension dependencies from release artifacts: ${releaseTarballInstall.tarballs.length} tarballs from ${releaseTarballInstall.artifactDir}`);
3266
+ }
3267
+ else {
3268
+ execSync("npm install --ignore-scripts", { cwd: extensionDir, stdio: "pipe" });
3269
+ steps.push("Ran npm install --ignore-scripts");
3270
+ const linkedPackages = installExtensionFromLocalWorkspaceBuild(extensionDir);
3271
+ if (linkedPackages !== null) {
3272
+ steps.push(`Linked coherent local workspace packages: ${linkedPackages.join(", ")}`);
3273
+ }
3274
+ }
3239
3275
  }
3240
- else {
3241
- execSync("npm install --ignore-scripts", { cwd: extensionDir, stdio: "pipe" });
3242
- steps.push("Ran npm install --ignore-scripts");
3276
+ catch (err) {
3277
+ const message = err instanceof Error ? err.message : String(err);
3278
+ if (releaseTarballInstall !== null) {
3279
+ throw new Error(`Extension dependency install from release artifacts failed: ${message}`);
3280
+ }
3243
3281
  const linkedPackages = installExtensionFromLocalWorkspaceBuild(extensionDir);
3244
3282
  if (linkedPackages !== null) {
3245
3283
  steps.push(`Linked coherent local workspace packages: ${linkedPackages.join(", ")}`);
3246
3284
  }
3285
+ else {
3286
+ steps.push(`npm install failed (non-fatal): ${message}`);
3287
+ }
3247
3288
  }
3289
+ const manifestPath = path.join(extensionDir, "openclaw.plugin.json");
3290
+ writeFileSync(manifestPath, buildExtensionPluginManifest(), "utf8");
3291
+ steps.push(`Wrote manifest: ${manifestPath}`);
3248
3292
  }
3249
- catch (err) {
3250
- const message = err instanceof Error ? err.message : String(err);
3251
- if (releaseTarballInstall !== null) {
3252
- throw new Error(`Extension dependency install from release artifacts failed: ${message}`);
3253
- }
3254
- const linkedPackages = installExtensionFromLocalWorkspaceBuild(extensionDir);
3255
- if (linkedPackages !== null) {
3256
- steps.push(`Linked coherent local workspace packages: ${linkedPackages.join(", ")}`);
3257
- }
3258
- else {
3259
- steps.push(`npm install failed (non-fatal): ${message}`);
3260
- }
3261
- }
3262
- // 8. Write plugin manifest
3263
- const manifestPath = path.join(extensionDir, "openclaw.plugin.json");
3264
- writeFileSync(manifestPath, buildExtensionPluginManifest(), "utf8");
3265
- steps.push(`Wrote manifest: ${manifestPath}`);
3266
3293
  const pluginConfigRepair = ensureOpenClawBrainPluginConfig(parsed.openclawHome);
3267
3294
  steps.push(pluginConfigRepair.detail);
3268
3295
  const learnerService = ensureLifecycleLearnerService(parsed.activationRoot);
@@ -3270,7 +3297,8 @@ function runProfileHookAttachCommand(parsed) {
3270
3297
  const brainFeedback = buildInstallBrainFeedbackSummary({
3271
3298
  parsed,
3272
3299
  targetInspection,
3273
- extensionDir,
3300
+ hookPath: installTarget.hookPath,
3301
+ hookLayout: installTarget.installLayout,
3274
3302
  activationPlan,
3275
3303
  learnerService,
3276
3304
  embedderProvision,
@@ -3291,7 +3319,7 @@ function runProfileHookAttachCommand(parsed) {
3291
3319
  : null
3292
3320
  ].filter((step) => step !== null);
3293
3321
  const preflightSummary = [
3294
- `Hook: installed at ${shortenPath(extensionDir)}`,
3322
+ `Hook: ${describeOpenClawBrainInstallLayout(installTarget.installLayout)} at ${shortenPath(installTarget.hookPath)}`,
3295
3323
  parsed.shared
3296
3324
  ? "Attachment policy: shared activation root declared; same-gateway many-profile load/serve proof is still not checked in"
3297
3325
  : "Attachment policy: dedicated activation root for this profile/home boundary",
@@ -3324,7 +3352,7 @@ function runProfileHookAttachCommand(parsed) {
3324
3352
  ? `Embedder: ensured default Ollama model ${embedderProvision.model} before brain init`
3325
3353
  : `Embedder: skipped default Ollama model ${embedderProvision.model} via ${parsed.skipEmbedderProvisionSource === "flag" ? "--skip-embedder-provision" : OPENCLAWBRAIN_INSTALL_SKIP_EMBEDDER_PROVISION_ENV}`,
3326
3354
  ...(providerDefaults === null ? [] : [`${providerDefaults.lifecycleSummary} (${shortenPath(providerDefaults.path)})`]),
3327
- `Profile hook: installed at ${shortenPath(extensionDir)}`,
3355
+ `Profile hook: ${describeOpenClawBrainInstallLayout(installTarget.installLayout)} at ${shortenPath(installTarget.hookPath)}`,
3328
3356
  `Learner service: ${learnerService.state} for ${shortenPath(parsed.activationRoot)}`,
3329
3357
  activationPlan.resolution === "new_root"
3330
3358
  ? `Activation data: initialized at ${shortenPath(parsed.activationRoot)}`
@@ -3401,6 +3429,7 @@ function runProfileHookAttachCommand(parsed) {
3401
3429
  learnerService,
3402
3430
  brainFeedback: {
3403
3431
  hookPath: brainFeedback.hookPath,
3432
+ hookLayout: brainFeedback.hookLayout,
3404
3433
  providerDefaultsPath: brainFeedback.providerDefaultsPath,
3405
3434
  profile: brainFeedback.profile,
3406
3435
  attachment: brainFeedback.attachment,
@@ -3480,6 +3509,10 @@ function readOpenClawJsonConfig(openclawHome) {
3480
3509
  }
3481
3510
  function ensureOpenClawBrainPluginConfig(openclawHome) {
3482
3511
  const { path: openclawJsonPath, config } = readOpenClawJsonConfig(openclawHome);
3512
+ const selectedInstall = findInstalledOpenClawBrainPlugin(openclawHome).selectedInstall;
3513
+ const knownPluginIds = selectedInstall === null
3514
+ ? ["openclawbrain", "openclaw"]
3515
+ : getOpenClawBrainKnownPluginIds(selectedInstall);
3483
3516
  const plugins = readJsonObjectRecord(config.plugins);
3484
3517
  if (plugins === null) {
3485
3518
  return {
@@ -3502,24 +3535,29 @@ function ensureOpenClawBrainPluginConfig(openclawHome) {
3502
3535
  detail: `Left ${shortenPath(openclawJsonPath)} unchanged because plugins.allow is not an array`
3503
3536
  };
3504
3537
  }
3505
- if (plugins.allow.includes("openclawbrain")) {
3538
+ const missingPluginIds = knownPluginIds.filter((pluginId) => !plugins.allow.includes(pluginId));
3539
+ if (missingPluginIds.length === 0) {
3506
3540
  return {
3507
3541
  path: openclawJsonPath,
3508
3542
  changed: false,
3509
- detail: `Verified ${shortenPath(openclawJsonPath)} plugins.allow already includes openclawbrain`
3543
+ detail: `Verified ${shortenPath(openclawJsonPath)} plugins.allow already includes ${knownPluginIds.join(", ")}`
3510
3544
  };
3511
3545
  }
3512
- plugins.allow = [...plugins.allow, "openclawbrain"];
3546
+ plugins.allow = [...plugins.allow, ...missingPluginIds];
3513
3547
  config.plugins = plugins;
3514
3548
  writeFileSync(openclawJsonPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
3515
3549
  return {
3516
3550
  path: openclawJsonPath,
3517
3551
  changed: true,
3518
- detail: `Repaired ${shortenPath(openclawJsonPath)} plugins.allow by adding openclawbrain`
3552
+ detail: `Repaired ${shortenPath(openclawJsonPath)} plugins.allow by adding ${missingPluginIds.join(", ")}`
3519
3553
  };
3520
3554
  }
3521
3555
  function scrubOpenClawBrainPluginConfig(openclawHome) {
3522
3556
  const { path: openclawJsonPath, config } = readOpenClawJsonConfig(openclawHome);
3557
+ const selectedInstall = findInstalledOpenClawBrainPlugin(openclawHome).selectedInstall;
3558
+ const knownPluginIds = new Set(selectedInstall === null
3559
+ ? ["openclawbrain", "openclaw"]
3560
+ : getOpenClawBrainKnownPluginIds(selectedInstall));
3523
3561
  const plugins = readJsonObjectRecord(config.plugins);
3524
3562
  if (plugins === null) {
3525
3563
  return {
@@ -3531,10 +3569,10 @@ function scrubOpenClawBrainPluginConfig(openclawHome) {
3531
3569
  const changes = [];
3532
3570
  let changed = false;
3533
3571
  if (Array.isArray(plugins.allow)) {
3534
- const filteredAllow = plugins.allow.filter((entry) => entry !== "openclawbrain");
3572
+ const filteredAllow = plugins.allow.filter((entry) => !knownPluginIds.has(entry));
3535
3573
  if (filteredAllow.length !== plugins.allow.length) {
3536
3574
  changed = true;
3537
- changes.push("removed plugins.allow entry");
3575
+ changes.push("removed plugins.allow entries");
3538
3576
  if (filteredAllow.length > 0) {
3539
3577
  plugins.allow = filteredAllow;
3540
3578
  }
@@ -3544,10 +3582,15 @@ function scrubOpenClawBrainPluginConfig(openclawHome) {
3544
3582
  }
3545
3583
  }
3546
3584
  const entries = readJsonObjectRecord(plugins.entries);
3547
- if (entries !== null && Object.prototype.hasOwnProperty.call(entries, "openclawbrain")) {
3548
- delete entries.openclawbrain;
3549
- changed = true;
3550
- changes.push("removed plugins.entries.openclawbrain");
3585
+ if (entries !== null) {
3586
+ for (const pluginId of knownPluginIds) {
3587
+ if (!Object.prototype.hasOwnProperty.call(entries, pluginId)) {
3588
+ continue;
3589
+ }
3590
+ delete entries[pluginId];
3591
+ changed = true;
3592
+ changes.push(`removed plugins.entries.${pluginId}`);
3593
+ }
3551
3594
  }
3552
3595
  if (entries !== null && Object.keys(entries).length === 0 && Object.prototype.hasOwnProperty.call(plugins, "entries")) {
3553
3596
  delete plugins.entries;
@@ -3584,14 +3627,29 @@ function resolveCleanupActivationRoot(openclawHome, explicitActivationRoot) {
3584
3627
  return resolved.trim().length === 0 ? null : path.resolve(resolved);
3585
3628
  }
3586
3629
  function removeProfileHookup(openclawHome, steps) {
3587
- const extensionDir = path.join(openclawHome, "extensions", "openclawbrain");
3588
- if (!existsSync(extensionDir)) {
3589
- steps.push(`Profile hookup already absent: ${extensionDir}`);
3590
- return extensionDir;
3630
+ const installedPlugin = findInstalledOpenClawBrainPlugin(openclawHome);
3631
+ const installs = installedPlugin.selectedInstall === null
3632
+ ? []
3633
+ : [installedPlugin.selectedInstall, ...installedPlugin.additionalInstalls];
3634
+ if (installs.length === 0) {
3635
+ steps.push(`Profile hookup already absent under ${installedPlugin.extensionsDir}`);
3636
+ return {
3637
+ primaryPath: path.join(openclawHome, "extensions", "openclawbrain"),
3638
+ removedPaths: []
3639
+ };
3640
+ }
3641
+ const removedPaths = [];
3642
+ for (const install of installs
3643
+ .slice()
3644
+ .sort((left, right) => right.extensionDir.length - left.extensionDir.length)) {
3645
+ rmSync(install.extensionDir, { recursive: true, force: true });
3646
+ removedPaths.push(install.extensionDir);
3647
+ steps.push(`Removed ${describeOpenClawBrainInstallLayout(install.installLayout)}: ${install.extensionDir}`);
3591
3648
  }
3592
- rmSync(extensionDir, { recursive: true, force: true });
3593
- steps.push(`Removed profile hookup: ${extensionDir}`);
3594
- return extensionDir;
3649
+ return {
3650
+ primaryPath: removedPaths[0],
3651
+ removedPaths
3652
+ };
3595
3653
  }
3596
3654
  function summarizeKeptActivationData(activationRoot) {
3597
3655
  if (activationRoot === null) {
@@ -3636,7 +3694,7 @@ function runDetachCommand(parsed) {
3636
3694
  clearCleanupRuntimeLoadProof(activationRoot, parsed.openclawHome, steps);
3637
3695
  const learnerService = resolveCleanupLearnerServiceOutcome(activationRoot, parsed.openclawHome);
3638
3696
  const pluginConfigCleanup = scrubOpenClawBrainPluginConfig(parsed.openclawHome);
3639
- const extensionDir = removeProfileHookup(parsed.openclawHome, steps);
3697
+ const removedHookup = removeProfileHookup(parsed.openclawHome, steps);
3640
3698
  const legacyResidue = removeLegacyProfileResidue(parsed.openclawHome);
3641
3699
  const activationData = summarizeKeptActivationData(activationRoot);
3642
3700
  const restartGuidance = buildRestartGuidance(parsed.restart);
@@ -3667,7 +3725,8 @@ function runDetachCommand(parsed) {
3667
3725
  profileSource: targetInspection.profileSource,
3668
3726
  configuredProfileIds: targetInspection.configuredProfileIds
3669
3727
  },
3670
- extensionDir,
3728
+ extensionDir: removedHookup.primaryPath,
3729
+ removedHookDirs: removedHookup.removedPaths,
3671
3730
  activationRoot,
3672
3731
  dataAction: "kept",
3673
3732
  activationDataState: activationData.activationDataState,
@@ -3727,7 +3786,7 @@ function runUninstallCommand(parsed) {
3727
3786
  learnerService.matchesRequestedActivationRoot !== false) {
3728
3787
  throw new Error(`Refusing to purge activation root ${path.resolve(activationRoot)} because the background learner service for this exact root could not be removed. ${learnerService.detail}`);
3729
3788
  }
3730
- const extensionDir = removeProfileHookup(parsed.openclawHome, steps);
3789
+ const removedHookup = removeProfileHookup(parsed.openclawHome, steps);
3731
3790
  const legacyResidue = removeLegacyProfileResidue(parsed.openclawHome);
3732
3791
  let activationData;
3733
3792
  if (parsed.dataMode === "purge") {
@@ -3785,7 +3844,8 @@ function runUninstallCommand(parsed) {
3785
3844
  profileSource: targetInspection.profileSource,
3786
3845
  configuredProfileIds: targetInspection.configuredProfileIds
3787
3846
  },
3788
- extensionDir,
3847
+ extensionDir: removedHookup.primaryPath,
3848
+ removedHookDirs: removedHookup.removedPaths,
3789
3849
  activationRoot,
3790
3850
  dataAction: parsed.dataMode,
3791
3851
  activationDataState: activationData.activationDataState,
@@ -5520,4 +5580,4 @@ if (isDirectCliRun(process.argv[1], import.meta.url)) {
5520
5580
  process.exitCode = 1;
5521
5581
  }
5522
5582
  }
5523
- //# sourceMappingURL=cli.js.map
5583
+ //# sourceMappingURL=cli.js.map
@@ -1561,9 +1561,16 @@ export interface OperatorRouteFnFreshnessSummary {
1561
1561
  interface OperatorHookSummary {
1562
1562
  scope: "exact_openclaw_home" | "activation_root_only";
1563
1563
  openclawHome: string | null;
1564
+ extensionDir: string | null;
1564
1565
  hookPath: string | null;
1565
1566
  runtimeGuardPath: string | null;
1566
1567
  manifestPath: string | null;
1568
+ packageJsonPath: string | null;
1569
+ manifestId: string | null;
1570
+ installId: string | null;
1571
+ packageName: string | null;
1572
+ installLayout: import("./openclaw-plugin-install.js").OpenClawBrainInstallLayout | null;
1573
+ additionalInstallCount: number;
1567
1574
  installState: CurrentProfileHookInstallStateV1;
1568
1575
  loadability: CurrentProfileHookLoadabilityV1;
1569
1576
  loadProof: CurrentProfileHookLoadProofV1;