@openclawbrain/openclaw 0.3.2 → 0.3.3

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/dist/src/cli.js CHANGED
@@ -13,7 +13,8 @@ import { buildTeacherSupervisionArtifactsFromNormalizedEventExport, createAlways
13
13
  import { inspectActivationState, loadPackFromActivation, promoteCandidatePack, readLearningSpineLogEntries, stageCandidatePack } from "@openclawbrain/pack-format";
14
14
  import { resolveActivationRoot } from "./resolve-activation-root.js";
15
15
  import { describeOpenClawHomeInspection, discoverOpenClawHomes, formatOpenClawHomeLayout, formatOpenClawHomeProfileSource, inspectOpenClawHome } from "./openclaw-home-layout.js";
16
- import { DEFAULT_WATCH_POLL_INTERVAL_SECONDS, buildNormalizedEventExportFromScannedEvents, bootstrapRuntimeAttach, buildOperatorSurfaceReport, compileRuntimeContext, createAsyncTeacherLiveLoop, createOpenClawLocalSessionTail, createRuntimeEventExportScanner, describeCurrentProfileBrainStatus, formatOperatorRollbackReport, loadWatchTeacherSnapshotState, loadRuntimeEventExportBundle, persistWatchTeacherSnapshot, rollbackRuntimeAttach, resolveOperatorTeacherSnapshotPath, resolveAsyncTeacherLiveLoopSnapshotPath, resolveWatchSessionTailCursorPath, resolveWatchStateRoot, resolveWatchTeacherSnapshotPath, scanLiveEventExport, scanRecordedSession, summarizeLearningPathFromMaterialization, summarizeNormalizedEventExportLabelFlow, writeScannedEventExportBundle } from "./index.js";
16
+ import { inspectOpenClawBrainHookStatus, inspectOpenClawBrainPluginAllowlist } from "./openclaw-hook-truth.js";
17
+ 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";
17
18
  import { appendLearningUpdateLogs } from "./learning-spine.js";
18
19
  import { buildPassiveLearningSessionExportFromOpenClawSessionStore } from "./local-session-passive-learning.js";
19
20
  import { discoverOpenClawSessionStores, loadOpenClawSessionIndex, readOpenClawSessionFile } from "./session-store.js";
@@ -38,6 +39,15 @@ function normalizeOptionalCliString(value) {
38
39
  const trimmed = value.trim();
39
40
  return trimmed.length > 0 ? trimmed : null;
40
41
  }
42
+ function canonicalizeExistingCliPath(filePath) {
43
+ const resolvedPath = path.resolve(filePath);
44
+ try {
45
+ return realpathSync(resolvedPath);
46
+ }
47
+ catch {
48
+ return resolvedPath;
49
+ }
50
+ }
41
51
  function readTruthyEnvFlag(name, env = process.env) {
42
52
  const value = normalizeOptionalCliString(env[name]);
43
53
  if (value === null) {
@@ -51,6 +61,45 @@ function getCliHomeDir() {
51
61
  function discoverInstallCandidateOpenClawHomes(homeDir = getCliHomeDir()) {
52
62
  return discoverOpenClawHomes(homeDir).map((inspection) => inspection.openclawHome);
53
63
  }
64
+ function summarizeSharedActivationRootReferenceProof(reference) {
65
+ switch (reference.installState) {
66
+ case "installed":
67
+ return "hook installed and loadable";
68
+ case "blocked_by_allowlist":
69
+ return "hook files exist but OpenClaw will not load them because plugins.allow blocks openclawbrain";
70
+ case "not_installed":
71
+ default:
72
+ return "hook files are incomplete, so serve-path attachment is not proven";
73
+ }
74
+ }
75
+ function partitionSharedActivationRootHookReferences(references) {
76
+ return references.reduce((result, reference) => {
77
+ if (reference.serveAttachmentState === "attached") {
78
+ result.attached.push(reference);
79
+ }
80
+ else {
81
+ result.halfAttached.push(reference);
82
+ }
83
+ return result;
84
+ }, {
85
+ attached: [],
86
+ halfAttached: []
87
+ });
88
+ }
89
+ function formatSharedActivationRootReferenceList(references, options = {}) {
90
+ return references
91
+ .map((reference) => {
92
+ const parts = [path.resolve(reference.openclawHome)];
93
+ if (options.includeInspection) {
94
+ parts.push(describeOpenClawHomeInspection(reference.inspection));
95
+ }
96
+ if (options.includeProof) {
97
+ parts.push(summarizeSharedActivationRootReferenceProof(reference));
98
+ }
99
+ return ` - ${parts.join(" | ")}`;
100
+ })
101
+ .join("\n");
102
+ }
54
103
  function findInstalledHookReferencesForActivationRoot(input) {
55
104
  const resolvedActivationRoot = path.resolve(input.activationRoot);
56
105
  const resolvedExcludedHome = input.excludingOpenClawHome === undefined || input.excludingOpenClawHome === null
@@ -66,8 +115,18 @@ function findInstalledHookReferencesForActivationRoot(input) {
66
115
  if (installedActivationRoot.trim().length === 0) {
67
116
  return [];
68
117
  }
118
+ const installHook = summarizeStatusInstallHook(inspection.openclawHome);
119
+ const reference = {
120
+ openclawHome: inspection.openclawHome,
121
+ inspection,
122
+ installState: installHook.state === "installed" || installHook.state === "blocked_by_allowlist"
123
+ ? installHook.state
124
+ : "not_installed",
125
+ serveAttachmentState: installHook.state === "installed" ? "attached" : "half_attached",
126
+ hookDetail: installHook.detail
127
+ };
69
128
  return path.resolve(installedActivationRoot) === resolvedActivationRoot
70
- ? [{ openclawHome: inspection.openclawHome, inspection }]
129
+ ? [reference]
71
130
  : [];
72
131
  })
73
132
  .sort((left, right) => left.openclawHome.localeCompare(right.openclawHome));
@@ -76,11 +135,18 @@ function findOtherInstalledHookReferencesForActivationRoot(input) {
76
135
  return findInstalledHookReferencesForActivationRoot(input);
77
136
  }
78
137
  function resolveWatchProfileRootsForActivationRoot(activationRoot, homeDir = getCliHomeDir()) {
79
- const attachedProfileRoots = findInstalledHookReferencesForActivationRoot({
138
+ const references = findInstalledHookReferencesForActivationRoot({
80
139
  activationRoot,
81
140
  homeDir
82
- }).map((reference) => path.resolve(reference.openclawHome));
83
- return attachedProfileRoots.length > 0 ? attachedProfileRoots : undefined;
141
+ });
142
+ const partitioned = partitionSharedActivationRootHookReferences(references);
143
+ const attachedProfileRoots = partitioned.attached.map((reference) => path.resolve(reference.openclawHome));
144
+ return {
145
+ attachedProfileRoots: attachedProfileRoots.length > 0 || references.length > 0
146
+ ? attachedProfileRoots
147
+ : undefined,
148
+ halfAttachedReferences: partitioned.halfAttached
149
+ };
84
150
  }
85
151
  function assertActivationRootPurgeIsNotShared(input) {
86
152
  const sharedReferences = findOtherInstalledHookReferencesForActivationRoot({
@@ -90,16 +156,36 @@ function assertActivationRootPurgeIsNotShared(input) {
90
156
  if (sharedReferences.length === 0) {
91
157
  return;
92
158
  }
93
- const attachedProfiles = sharedReferences
94
- .map(({ openclawHome, inspection }) => ` - ${path.resolve(openclawHome)} (${describeOpenClawHomeInspection(inspection)})`)
95
- .join("\n");
96
- throw new Error([
97
- `Refusing to purge activation root ${path.resolve(input.activationRoot)} because another installed OpenClaw profile still points at it.`,
98
- "Other attached profiles:",
99
- attachedProfiles,
100
- "Use uninstall --keep-data or detach on this profile first, then remove the remaining profile hooks before purging shared brain data.",
101
- "For Eagle dogfood, prefer its own activation root so CormorantAI stays untouched."
102
- ].join("\n"));
159
+ const partitioned = partitionSharedActivationRootHookReferences(sharedReferences);
160
+ const attachedProfiles = formatSharedActivationRootReferenceList(partitioned.attached, {
161
+ includeInspection: true,
162
+ includeProof: true
163
+ });
164
+ const halfAttachedProfiles = formatSharedActivationRootReferenceList(partitioned.halfAttached, {
165
+ includeInspection: true,
166
+ includeProof: true
167
+ });
168
+ throw new Error(partitioned.attached.length > 0
169
+ ? [
170
+ `Refusing to purge activation root ${path.resolve(input.activationRoot)} because another installed OpenClaw profile still points at it.`,
171
+ "Other attached profiles:",
172
+ attachedProfiles,
173
+ ...(partitioned.halfAttached.length === 0
174
+ ? []
175
+ : [
176
+ "Other half-attached profiles:",
177
+ halfAttachedProfiles
178
+ ]),
179
+ "Use uninstall --keep-data or detach on this profile first, then remove or repair the remaining profile hooks before purging shared brain data.",
180
+ "For Eagle dogfood, prefer its own activation root so CormorantAI stays untouched."
181
+ ].join("\n")
182
+ : [
183
+ `Refusing to purge activation root ${path.resolve(input.activationRoot)} because another OpenClaw profile still points at it, but its hook is not loadable yet.`,
184
+ "Other half-attached profiles:",
185
+ halfAttachedProfiles,
186
+ "Repair or remove those half-attached profile hooks before purging shared brain data so status stays explicit instead of drifting into a missing-root surprise.",
187
+ "For Eagle dogfood, prefer its own activation root so CormorantAI stays untouched."
188
+ ].join("\n"));
103
189
  }
104
190
  function formatInstallOpenClawHomeSource(source) {
105
191
  switch (source) {
@@ -541,6 +627,8 @@ function formatCompactList(values, maxItems = 2, maxLength = 64) {
541
627
  return values.length > maxItems ? `${visible.join("|")}+${values.length - maxItems}more` : visible.join("|");
542
628
  }
543
629
  const SERVICE_RISK_FINDING_CODES = new Set([
630
+ "hook_desynced",
631
+ "current_profile_not_attached",
544
632
  "activation_broken_install",
545
633
  "activation_stale_incomplete",
546
634
  "active_missing",
@@ -551,6 +639,7 @@ const SERVICE_RISK_FINDING_CODES = new Set([
551
639
  "serve_path_route_evidence_missing"
552
640
  ]);
553
641
  const DEGRADED_BRAIN_FINDING_CODES = new Set([
642
+ "attachment_scope_partial",
554
643
  "bootstrap_waiting_for_first_export",
555
644
  "serve_path_unprobed",
556
645
  "brain_context_kernel_only",
@@ -586,34 +675,180 @@ const TEACHER_NO_OP_MESSAGES = {
586
675
  unavailable: "the latest cycle is not visible from the current operator snapshot"
587
676
  };
588
677
  function summarizeStatusInstallHook(openclawHome) {
678
+ const hook = inspectOpenClawBrainHookStatus(openclawHome);
679
+ return {
680
+ state: hook.installState === "unverified" ? "unknown" : hook.installState,
681
+ detail: hook.detail
682
+ };
683
+ }
684
+ function summarizeStatusHookLoad(installHook, status) {
685
+ return {
686
+ installState: installHook.state === "unknown" ? "unverified" : installHook.state,
687
+ loadProof: status.hook.loadProof,
688
+ detail: status.hook.detail
689
+ };
690
+ }
691
+ function summarizeStatusConfigLoad(openclawHome) {
589
692
  if (openclawHome === null) {
590
693
  return {
591
- state: "unknown",
592
- detail: "profile hook state is unknown from activation-root-only status; pin --openclaw-home to prove install state"
694
+ state: "unverified",
695
+ detail: "plugin allowlist state is unknown from activation-root-only status; pin --openclaw-home to prove config load state"
593
696
  };
594
697
  }
595
- const extensionDir = path.join(path.resolve(openclawHome), "extensions", "openclawbrain");
596
- const indexPath = path.join(extensionDir, "index.ts");
597
- const runtimeGuardPath = path.join(extensionDir, "runtime-guard.js");
598
- const manifestPath = path.join(extensionDir, "openclaw.plugin.json");
599
- if (existsSync(indexPath) && existsSync(runtimeGuardPath) && existsSync(manifestPath)) {
698
+ const allowlist = inspectOpenClawBrainPluginAllowlist(openclawHome);
699
+ if (allowlist.state === "blocked") {
600
700
  return {
601
- state: "installed",
602
- detail: `profile hook is installed at ${shortenPath(extensionDir)}`
701
+ state: "blocked",
702
+ detail: allowlist.detail
703
+ };
704
+ }
705
+ if (allowlist.state === "invalid") {
706
+ return {
707
+ state: "invalid",
708
+ detail: allowlist.detail
603
709
  };
604
710
  }
605
711
  return {
606
- state: "not_installed",
607
- detail: `profile hook is not present at ${shortenPath(extensionDir)}`
712
+ state: "allows_load",
713
+ detail: allowlist.detail
608
714
  };
609
715
  }
610
- function summarizeStatusHookLoad(installHook, status) {
716
+ function summarizeStatusHookFilesState(installHook) {
717
+ if (installHook.state === "installed" || installHook.state === "blocked_by_allowlist") {
718
+ return "present";
719
+ }
720
+ if (installHook.state === "not_installed") {
721
+ return "missing";
722
+ }
723
+ return "unverified";
724
+ }
725
+ function summarizeStatusAttachmentWatcher(status) {
726
+ if (status.passiveLearning.watchState === "watching") {
727
+ return "alive";
728
+ }
729
+ if (status.passiveLearning.watchState === "stale_snapshot") {
730
+ return "stale";
731
+ }
732
+ return "not_visible";
733
+ }
734
+ function formatAttachedProfileIdentity(reference) {
735
+ const profileLabel = reference.inspection.profileId ?? "current_profile";
736
+ return `${profileLabel}@${shortenPath(path.resolve(reference.openclawHome))}`;
737
+ }
738
+ function buildStatusAttachedProfileTruth(input) {
739
+ const resolvedOpenClawHome = canonicalizeExistingCliPath(input.reference.openclawHome);
740
+ const installHook = summarizeStatusInstallHook(resolvedOpenClawHome);
741
+ const configLoad = summarizeStatusConfigLoad(resolvedOpenClawHome);
742
+ const runtimeProof = input.runtimeProofByHome.get(resolvedOpenClawHome);
743
+ const currentOpenClawHome = input.currentOpenClawHome === null ? null : canonicalizeExistingCliPath(input.currentOpenClawHome);
744
+ const hookFiles = summarizeStatusHookFilesState(installHook);
611
745
  return {
612
- installState: installHook.state === "unknown" ? "unverified" : installHook.state,
613
- loadProof: status.attachment.state === "attached" && status.brainStatus.serveState === "serving_active_pack"
614
- ? "status_probe_ready"
615
- : "not_ready",
616
- detail: installHook.detail
746
+ label: formatAttachedProfileIdentity(input.reference),
747
+ openclawHome: resolvedOpenClawHome,
748
+ current: currentOpenClawHome !== null && currentOpenClawHome === resolvedOpenClawHome,
749
+ hookFiles,
750
+ configLoad: configLoad.state,
751
+ runtimeLoad: input.runtimeProofError !== null
752
+ ? "proof_error"
753
+ : hookFiles !== "present"
754
+ ? "not_proven"
755
+ : runtimeProof !== undefined
756
+ ? "proven"
757
+ : "not_proven",
758
+ runtimeLoadedAt: runtimeProof?.loadedAt ?? null
759
+ };
760
+ }
761
+ function buildCurrentStatusAttachmentTruthDetail(input) {
762
+ const attachedProfileCount = input.attachedProfiles.length;
763
+ const attachedProfilesDetail = attachedProfileCount === 0
764
+ ? "no attached profiles were discovered for this activation root"
765
+ : `attached profile set has ${attachedProfileCount} discoverable ${attachedProfileCount === 1 ? "profile" : "profiles"}`;
766
+ if (input.openclawHome === null) {
767
+ return ("current-profile hook/config/runtime load truth is unverified without --openclaw-home; " +
768
+ `${attachedProfilesDetail}`);
769
+ }
770
+ if (input.runtimeProofError !== null) {
771
+ return (`current profile ${input.currentProfileLabel} could not prove runtime load because ${shortenPath(input.runtimeProofPath)} is unreadable: ${input.runtimeProofError}; ` +
772
+ `${attachedProfilesDetail}`);
773
+ }
774
+ const hookDetail = input.hookFiles === "present"
775
+ ? "hook files are present"
776
+ : input.hookFiles === "missing"
777
+ ? "hook files are missing"
778
+ : "hook files are unverified";
779
+ const configDetail = input.configLoad === "allows_load"
780
+ ? "config allows load"
781
+ : input.configLoad === "blocked"
782
+ ? "config blocks load"
783
+ : input.configLoad === "invalid"
784
+ ? "config is invalid for load proof"
785
+ : "config load is unverified";
786
+ const runtimeDetail = input.runtimeLoad === "proven"
787
+ ? "runtime load is proven"
788
+ : input.runtimeLoad === "not_proven"
789
+ ? "runtime load is not yet proven"
790
+ : input.runtimeLoad === "proof_error"
791
+ ? "runtime load proof is broken"
792
+ : "runtime load is unverified";
793
+ const watcherDetail = input.watcher === "alive"
794
+ ? "watcher is alive"
795
+ : input.watcher === "stale"
796
+ ? "watcher visibility is stale"
797
+ : "watcher is not visible";
798
+ return `current profile ${input.currentProfileLabel}: ${hookDetail}, ${configDetail}, ${runtimeDetail}, ${watcherDetail}; ${attachedProfilesDetail}`;
799
+ }
800
+ function summarizeStatusAttachmentTruth(input) {
801
+ const runtimeProofs = listOpenClawProfileRuntimeLoadProofs(input.activationRoot);
802
+ const runtimeProofByHome = new Map();
803
+ for (const proof of runtimeProofs.proofs?.profiles ?? []) {
804
+ runtimeProofByHome.set(canonicalizeExistingCliPath(proof.openclawHome), {
805
+ loadedAt: proof.loadedAt
806
+ });
807
+ }
808
+ const attachedProfiles = findInstalledHookReferencesForActivationRoot({
809
+ activationRoot: input.activationRoot
810
+ }).map((reference) => buildStatusAttachedProfileTruth({
811
+ reference,
812
+ currentOpenClawHome: input.openclawHome,
813
+ runtimeProofByHome,
814
+ runtimeProofError: runtimeProofs.error
815
+ }));
816
+ const currentInspection = input.openclawHome === null ? null : inspectOpenClawHome(input.openclawHome);
817
+ const currentProfileLabel = currentInspection?.profileId ?? "current_profile";
818
+ const installHook = summarizeStatusInstallHook(input.openclawHome);
819
+ const configLoad = summarizeStatusConfigLoad(input.openclawHome);
820
+ const currentRuntimeProof = input.openclawHome === null ? undefined : runtimeProofByHome.get(canonicalizeExistingCliPath(input.openclawHome));
821
+ const hookFiles = summarizeStatusHookFilesState(installHook);
822
+ const watcher = summarizeStatusAttachmentWatcher(input.status);
823
+ const runtimeLoad = input.openclawHome === null
824
+ ? "unverified"
825
+ : runtimeProofs.error !== null
826
+ ? "proof_error"
827
+ : hookFiles !== "present"
828
+ ? "not_proven"
829
+ : currentRuntimeProof !== undefined
830
+ ? "proven"
831
+ : "not_proven";
832
+ return {
833
+ currentProfileLabel,
834
+ hookFiles,
835
+ configLoad: configLoad.state,
836
+ runtimeLoad,
837
+ watcher,
838
+ attachedProfiles,
839
+ runtimeProofPath: runtimeProofs.path,
840
+ runtimeProofError: runtimeProofs.error,
841
+ detail: buildCurrentStatusAttachmentTruthDetail({
842
+ openclawHome: input.openclawHome,
843
+ currentProfileLabel,
844
+ hookFiles,
845
+ configLoad: configLoad.state,
846
+ runtimeLoad,
847
+ watcher,
848
+ attachedProfiles,
849
+ runtimeProofPath: runtimeProofs.path,
850
+ runtimeProofError: runtimeProofs.error
851
+ })
617
852
  };
618
853
  }
619
854
  function runOllamaProbe(args, baseUrl) {
@@ -988,6 +1223,28 @@ function formatStatusObservedDeltaTransition(delta) {
988
1223
  }
989
1224
  return `${delta.latestPackTransition.kind}:${delta.latestPackTransition.fromPackId ?? "none"}->${delta.latestPackTransition.toPackId}`;
990
1225
  }
1226
+ function formatAttachedProfileTruthCompact(entry) {
1227
+ const currentPrefix = entry.current ? "*" : "";
1228
+ return (`${currentPrefix}${entry.label}` +
1229
+ `[hook=${entry.hookFiles} config=${entry.configLoad} runtime=${entry.runtimeLoad}` +
1230
+ `${entry.runtimeLoadedAt === null ? "" : `@${entry.runtimeLoadedAt}`}]`);
1231
+ }
1232
+ function formatAttachedProfileTruthCompactList(entries) {
1233
+ return entries.length === 0
1234
+ ? "none"
1235
+ : formatCompactList(entries.map((entry) => formatAttachedProfileTruthCompact(entry)), 2, 80);
1236
+ }
1237
+ function formatAttachedProfileTruthDetailedList(entries) {
1238
+ return entries.length === 0
1239
+ ? "none"
1240
+ : entries
1241
+ .map((entry) => `${entry.current ? "*" : ""}${entry.label}` +
1242
+ `[hook=${entry.hookFiles} config=${entry.configLoad} runtime=${entry.runtimeLoad} loadedAt=${entry.runtimeLoadedAt ?? "none"}]`)
1243
+ .join(" ");
1244
+ }
1245
+ function summarizeDisplayedStatus(status, installHook) {
1246
+ return installHook.state === "blocked_by_allowlist" ? "fail" : status.brainStatus.status;
1247
+ }
991
1248
  function buildCompactStatusHeader(status, report, options) {
992
1249
  const installHook = summarizeStatusInstallHook(options.openclawHome);
993
1250
  const hookLoad = summarizeStatusHookLoad(installHook, status);
@@ -998,9 +1255,15 @@ function buildCompactStatusHeader(status, report, options) {
998
1255
  const routeFn = summarizeStatusRouteFn(status, report);
999
1256
  const alerts = summarizeStatusAlerts(report, options.providerConfig, embeddings, localLlm);
1000
1257
  const liveModels = embeddings.models.length === 0 ? "none" : embeddings.models.join("|");
1258
+ const attachmentTruth = summarizeStatusAttachmentTruth({
1259
+ activationRoot: status.host.activationRoot,
1260
+ openclawHome: options.openclawHome,
1261
+ status
1262
+ });
1001
1263
  return [
1002
1264
  `lifecycle attach=${status.attachment.state} learner=${yesNo(status.passiveLearning.learnerRunning)} watch=${summarizeStatusWatchState(status)} export=${status.passiveLearning.exportState} promote=${summarizeStatusPromotionState(status)} serve=${summarizeStatusServeReality(status)}`,
1003
1265
  `hook install=${hookLoad.installState} loadProof=${hookLoad.loadProof} detail=${hookLoad.detail}`,
1266
+ `attachTruth current=${attachmentTruth.currentProfileLabel} hook=${attachmentTruth.hookFiles} config=${attachmentTruth.configLoad} runtime=${attachmentTruth.runtimeLoad} watcher=${attachmentTruth.watcher} attachedSet=${formatAttachedProfileTruthCompactList(attachmentTruth.attachedProfiles)} why=${attachmentTruth.detail}`,
1004
1267
  `passive firstExport=${yesNo(status.passiveLearning.firstExportOccurred)} backlog=${status.passiveLearning.backlogState} pending=${formatStatusNullableNumber(status.passiveLearning.pendingLive)}/${formatStatusNullableNumber(status.passiveLearning.pendingBackfill)}`,
1005
1268
  `serving pack=${status.passiveLearning.currentServingPackId ?? "none"} lastExport=${status.passiveLearning.lastExportAt ?? "none"} lastPromotion=${status.passiveLearning.lastPromotionAt ?? "none"}`,
1006
1269
  `timing ${formatStatusHotPathTiming(status.brainStatus.timing)}`,
@@ -1017,21 +1280,34 @@ function buildCompactStatusHeader(status, report, options) {
1017
1280
  ];
1018
1281
  }
1019
1282
  function formatCurrentProfileStatusSummary(status, report, targetInspection, options) {
1283
+ const installHook = summarizeStatusInstallHook(options.openclawHome);
1284
+ const displayedStatus = summarizeDisplayedStatus(status, installHook);
1020
1285
  const embeddings = summarizeStatusEmbeddings(report, options.providerConfig);
1021
1286
  const localLlm = summarizeStatusLocalLlm(options.providerConfig);
1022
1287
  const liveModels = embeddings.models.length === 0 ? "none" : embeddings.models.join("|");
1288
+ const attachmentTruth = summarizeStatusAttachmentTruth({
1289
+ activationRoot: status.host.activationRoot,
1290
+ openclawHome: options.openclawHome,
1291
+ status
1292
+ });
1023
1293
  const profileIdSuffix = status.profile.profileId === null ? "" : ` id=${status.profile.profileId}`;
1024
1294
  const targetLine = targetInspection === null
1025
1295
  ? `target activation=${status.host.activationRoot} source=activation_root_only`
1026
1296
  : `target activation=${status.host.activationRoot} ${formatOpenClawTargetLine(targetInspection)} hook=${shortenPath(path.join(targetInspection.openclawHome, "extensions", "openclawbrain", "index.ts"))}`;
1027
1297
  return [
1028
- `STATUS ${status.brainStatus.status}`,
1298
+ `STATUS ${displayedStatus}`,
1029
1299
  ...buildCompactStatusHeader(status, report, options),
1030
1300
  `answer ${status.brain.summary}`,
1031
1301
  targetLine,
1032
1302
  ...(targetInspection === null ? [] : [`preflight ${formatOpenClawTargetExplanation(targetInspection)}`]),
1303
+ `next ${buildStatusNextStep(status, report, {
1304
+ openclawHome: options.openclawHome,
1305
+ installHook
1306
+ })}`,
1033
1307
  `host runtime=${status.host.runtimeOwner} activation=${status.host.activationRoot}`,
1034
1308
  `profile selector=${status.profile.selector}${profileIdSuffix} attachment=${status.attachment.state} policy=${status.attachment.policyMode}`,
1309
+ `attachTruth current=${attachmentTruth.currentProfileLabel} hook=${attachmentTruth.hookFiles} config=${attachmentTruth.configLoad} runtime=${attachmentTruth.runtimeLoad} watcher=${attachmentTruth.watcher} detail=${attachmentTruth.detail}`,
1310
+ `attachedSet ${formatAttachedProfileTruthDetailedList(attachmentTruth.attachedProfiles)} proofPath=${shortenPath(attachmentTruth.runtimeProofPath)} proofError=${attachmentTruth.runtimeProofError ?? "none"}`,
1035
1311
  `manyProfile surface=${report.manyProfile.operatorSurface} policy=${report.manyProfile.declaredAttachmentPolicy} intent=${report.manyProfile.sameGatewayIntent} checkedProof=${report.manyProfile.checkedInProofTopology} sameGatewayProof=${yesNo(report.manyProfile.sameGatewayProof)} sharedWriteProof=${yesNo(report.manyProfile.sharedWriteSafetyProof)}`,
1036
1312
  `activation state=${status.brainStatus.activationState} detail=${status.brain.detail}`,
1037
1313
  `brain pack=${status.brain.activePackId ?? "none"} state=${status.brain.state} init=${status.brain.initMode ?? "unknown"} routeFreshness=${status.brain.routeFreshness} lastPromotion=${status.brain.lastPromotionAt ?? "none"} router=${status.brain.routerIdentity ?? "none"}`,
@@ -1472,6 +1748,19 @@ function buildCleanupRestartGuidance(restart) {
1472
1748
  }
1473
1749
  function buildStatusNextStep(status, report, options) {
1474
1750
  const activationRootArg = quoteShellArg(status.host.activationRoot);
1751
+ const attachmentTruth = summarizeStatusAttachmentTruth({
1752
+ activationRoot: status.host.activationRoot,
1753
+ openclawHome: options.openclawHome,
1754
+ status
1755
+ });
1756
+ if (options.installHook.state === "blocked_by_allowlist") {
1757
+ if (options.openclawHome === null) {
1758
+ return "Repair the OpenClaw plugin allowlist mismatch before trusting serve-path status again.";
1759
+ }
1760
+ return ("Repair the OpenClaw plugin allowlist mismatch " +
1761
+ `(rerun ${buildInstallCommand(options.openclawHome)} or ${buildAttachCommand(options.openclawHome, status.host.activationRoot)}) ` +
1762
+ "before trusting serve-path status again.");
1763
+ }
1475
1764
  if (status.brainStatus.activationState === "broken_install") {
1476
1765
  return "Repair or replace the activation root before trusting serve-path status again.";
1477
1766
  }
@@ -1484,12 +1773,21 @@ function buildStatusNextStep(status, report, options) {
1484
1773
  if (options.openclawHome !== null && options.installHook.state === "not_installed") {
1485
1774
  return `Run \`${buildInstallCommand(options.openclawHome)}\` before expecting this OpenClaw home to load the brain hook.`;
1486
1775
  }
1776
+ if (options.openclawHome !== null &&
1777
+ attachmentTruth.hookFiles === "present" &&
1778
+ attachmentTruth.configLoad === "allows_load" &&
1779
+ attachmentTruth.runtimeLoad === "not_proven") {
1780
+ return "Restart the exact OpenClaw profile or wait for its next launch, then rerun status until runtime load becomes proven instead of assumed from on-disk state.";
1781
+ }
1487
1782
  if (status.brainStatus.awaitingFirstExport) {
1488
1783
  return `Let the attached OpenClaw profile emit a real export, then rerun \`openclawbrain status --activation-root ${activationRootArg}\`.`;
1489
1784
  }
1490
1785
  if (options.openclawHome === null) {
1491
1786
  return `Pin \`--openclaw-home <path>\` when you need exact hook-install truth; activation-root-only status only proves this root's serve-path state.`;
1492
1787
  }
1788
+ if (attachmentTruth.runtimeLoad === "proof_error") {
1789
+ return "Repair the runtime-load proof file before trusting attach truth again; status now knows the exact file that broke.";
1790
+ }
1493
1791
  if (options.installHook.state === "installed" && status.brainStatus.serveState === "serving_active_pack") {
1494
1792
  return "Check the OpenClaw startup log for the `[openclawbrain] BRAIN LOADED` breadcrumb when you need live hook-load proof.";
1495
1793
  }
@@ -1503,8 +1801,10 @@ function buildStatusNextStep(status, report, options) {
1503
1801
  return `Use \`openclawbrain status --activation-root ${activationRootArg} --detailed\` when you need the full lifecycle, serve-path, and backlog proof.`;
1504
1802
  }
1505
1803
  function formatHumanFriendlyStatus(status, report, targetInspection, options) {
1804
+ const installHook = summarizeStatusInstallHook(options.openclawHome);
1805
+ const displayedStatus = summarizeDisplayedStatus(status, installHook);
1506
1806
  const lines = [
1507
- `STATUS ${status.brainStatus.status}`,
1807
+ `STATUS ${displayedStatus}`,
1508
1808
  ...buildCompactStatusHeader(status, report, options),
1509
1809
  ...(targetInspection === null ? [] : [
1510
1810
  `target ${formatOpenClawTargetLine(targetInspection)}`,
@@ -1512,7 +1812,7 @@ function formatHumanFriendlyStatus(status, report, targetInspection, options) {
1512
1812
  ]),
1513
1813
  `next ${buildStatusNextStep(status, report, {
1514
1814
  openclawHome: options.openclawHome,
1515
- installHook: summarizeStatusInstallHook(options.openclawHome)
1815
+ installHook
1516
1816
  })}`
1517
1817
  ];
1518
1818
  return lines.join("\n");
@@ -2642,14 +2942,20 @@ function resolveCleanupLearnerServiceOutcome(activationRoot, openclawHome) {
2642
2942
  activationRoot,
2643
2943
  excludingOpenClawHome: openclawHome
2644
2944
  });
2645
- if (remainingProfiles.length > 0) {
2945
+ const partitioned = partitionSharedActivationRootHookReferences(remainingProfiles);
2946
+ if (partitioned.attached.length > 0) {
2646
2947
  const inspection = inspectManagedLearnerService(activationRoot);
2647
- const attachedProfiles = remainingProfiles
2948
+ const attachedProfiles = partitioned.attached
2648
2949
  .map(({ openclawHome: profileHome }) => shortenPath(path.resolve(profileHome)))
2649
2950
  .join(", ");
2951
+ const halfAttachedNote = partitioned.halfAttached.length === 0
2952
+ ? ""
2953
+ : ` Half-attached hooks still point at this root but were not counted as attached: ${partitioned.halfAttached
2954
+ .map((reference) => `${shortenPath(path.resolve(reference.openclawHome))} (${summarizeSharedActivationRootReferenceProof(reference)})`)
2955
+ .join(", ")}.`;
2650
2956
  return {
2651
2957
  state: "preserved",
2652
- detail: `Preserved the background learner service for ${path.resolve(activationRoot)} because other attached OpenClaw profiles still share this activation root: ${attachedProfiles}.`,
2958
+ detail: `Preserved the background learner service for ${path.resolve(activationRoot)} because other attached OpenClaw profiles still share this activation root: ${attachedProfiles}.${halfAttachedNote}`,
2653
2959
  plistPath: inspection.plistPath,
2654
2960
  logPath: inspection.logPath,
2655
2961
  configuredActivationRoot: inspection.configuredActivationRoot,
@@ -2657,9 +2963,14 @@ function resolveCleanupLearnerServiceOutcome(activationRoot, openclawHome) {
2657
2963
  };
2658
2964
  }
2659
2965
  const outcome = removeManagedLearnerServiceForActivationRoot(activationRoot);
2966
+ const halfAttachedNote = partitioned.halfAttached.length === 0
2967
+ ? ""
2968
+ : ` Ignored half-attached OpenClaw profile hooks that still point at this activation root because they do not prove serve-path attachment: ${partitioned.halfAttached
2969
+ .map((reference) => `${shortenPath(path.resolve(reference.openclawHome))} (${summarizeSharedActivationRootReferenceProof(reference)})`)
2970
+ .join(", ")}.`;
2660
2971
  return {
2661
2972
  state: outcome.state,
2662
- detail: outcome.detail,
2973
+ detail: `${outcome.detail}${halfAttachedNote}`,
2663
2974
  plistPath: outcome.inspection.plistPath,
2664
2975
  logPath: outcome.inspection.logPath,
2665
2976
  configuredActivationRoot: outcome.inspection.configuredActivationRoot,
@@ -2952,6 +3263,8 @@ function runProfileHookAttachCommand(parsed) {
2952
3263
  const manifestPath = path.join(extensionDir, "openclaw.plugin.json");
2953
3264
  writeFileSync(manifestPath, buildExtensionPluginManifest(), "utf8");
2954
3265
  steps.push(`Wrote manifest: ${manifestPath}`);
3266
+ const pluginConfigRepair = ensureOpenClawBrainPluginConfig(parsed.openclawHome);
3267
+ steps.push(pluginConfigRepair.detail);
2955
3268
  const learnerService = ensureLifecycleLearnerService(parsed.activationRoot);
2956
3269
  steps.push(learnerService.detail);
2957
3270
  const brainFeedback = buildInstallBrainFeedbackSummary({
@@ -3084,6 +3397,7 @@ function runProfileHookAttachCommand(parsed) {
3084
3397
  teacherBaseUrl: providerDefaults.defaults.teacherBaseUrl ?? null,
3085
3398
  embedderBaseUrl: providerDefaults.defaults.embedderBaseUrl ?? null
3086
3399
  },
3400
+ pluginConfigRepair,
3087
3401
  learnerService,
3088
3402
  brainFeedback: {
3089
3403
  hookPath: brainFeedback.hookPath,
@@ -3164,6 +3478,46 @@ function readOpenClawJsonConfig(openclawHome) {
3164
3478
  config
3165
3479
  };
3166
3480
  }
3481
+ function ensureOpenClawBrainPluginConfig(openclawHome) {
3482
+ const { path: openclawJsonPath, config } = readOpenClawJsonConfig(openclawHome);
3483
+ const plugins = readJsonObjectRecord(config.plugins);
3484
+ if (plugins === null) {
3485
+ return {
3486
+ path: openclawJsonPath,
3487
+ changed: false,
3488
+ detail: `Left ${shortenPath(openclawJsonPath)} unchanged because plugins.allow is not configured`
3489
+ };
3490
+ }
3491
+ if (!Object.prototype.hasOwnProperty.call(plugins, "allow")) {
3492
+ return {
3493
+ path: openclawJsonPath,
3494
+ changed: false,
3495
+ detail: `Left ${shortenPath(openclawJsonPath)} unchanged because plugins.allow is not configured`
3496
+ };
3497
+ }
3498
+ if (!Array.isArray(plugins.allow)) {
3499
+ return {
3500
+ path: openclawJsonPath,
3501
+ changed: false,
3502
+ detail: `Left ${shortenPath(openclawJsonPath)} unchanged because plugins.allow is not an array`
3503
+ };
3504
+ }
3505
+ if (plugins.allow.includes("openclawbrain")) {
3506
+ return {
3507
+ path: openclawJsonPath,
3508
+ changed: false,
3509
+ detail: `Verified ${shortenPath(openclawJsonPath)} plugins.allow already includes openclawbrain`
3510
+ };
3511
+ }
3512
+ plugins.allow = [...plugins.allow, "openclawbrain"];
3513
+ config.plugins = plugins;
3514
+ writeFileSync(openclawJsonPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
3515
+ return {
3516
+ path: openclawJsonPath,
3517
+ changed: true,
3518
+ detail: `Repaired ${shortenPath(openclawJsonPath)} plugins.allow by adding openclawbrain`
3519
+ };
3520
+ }
3167
3521
  function scrubOpenClawBrainPluginConfig(openclawHome) {
3168
3522
  const { path: openclawJsonPath, config } = readOpenClawJsonConfig(openclawHome);
3169
3523
  const plugins = readJsonObjectRecord(config.plugins);
@@ -3256,12 +3610,30 @@ function summarizeKeptActivationData(activationRoot) {
3256
3610
  function buildRestartGuidance(restart) {
3257
3611
  return buildCleanupRestartGuidance(restart);
3258
3612
  }
3613
+ function clearCleanupRuntimeLoadProof(activationRoot, openclawHome, steps) {
3614
+ if (activationRoot === null) {
3615
+ return;
3616
+ }
3617
+ try {
3618
+ const cleared = clearOpenClawProfileRuntimeLoadProof({
3619
+ activationRoot,
3620
+ openclawHome
3621
+ });
3622
+ if (cleared) {
3623
+ steps.push(`Cleared runtime-load proof for ${shortenPath(openclawHome)} from ${shortenPath(resolveAttachmentRuntimeLoadProofsPath(activationRoot))}`);
3624
+ }
3625
+ }
3626
+ catch (error) {
3627
+ steps.push(`Runtime-load proof cleanup failed open at ${shortenPath(resolveAttachmentRuntimeLoadProofsPath(activationRoot))}: ${toErrorMessage(error)}`);
3628
+ }
3629
+ }
3259
3630
  function runDetachCommand(parsed) {
3260
3631
  const steps = [];
3261
3632
  validateOpenClawHome(parsed.openclawHome);
3262
3633
  const targetInspection = inspectOpenClawHome(parsed.openclawHome);
3263
3634
  steps.push(`Detected layout: ${formatOpenClawTargetExplanation(targetInspection)}`);
3264
3635
  const activationRoot = resolveCleanupActivationRoot(parsed.openclawHome, parsed.activationRoot);
3636
+ clearCleanupRuntimeLoadProof(activationRoot, parsed.openclawHome, steps);
3265
3637
  const learnerService = resolveCleanupLearnerServiceOutcome(activationRoot, parsed.openclawHome);
3266
3638
  const pluginConfigCleanup = scrubOpenClawBrainPluginConfig(parsed.openclawHome);
3267
3639
  const extensionDir = removeProfileHookup(parsed.openclawHome, steps);
@@ -3340,6 +3712,7 @@ function runUninstallCommand(parsed) {
3340
3712
  const targetInspection = inspectOpenClawHome(parsed.openclawHome);
3341
3713
  steps.push(`Detected layout: ${formatOpenClawTargetExplanation(targetInspection)}`);
3342
3714
  const activationRoot = resolveCleanupActivationRoot(parsed.openclawHome, parsed.activationRoot);
3715
+ clearCleanupRuntimeLoadProof(activationRoot, parsed.openclawHome, steps);
3343
3716
  if (parsed.dataMode === "purge" && activationRoot !== null) {
3344
3717
  assertActivationRootPurgeIsNotShared({
3345
3718
  activationRoot,
@@ -4565,14 +4938,23 @@ export async function createWatchCommandRuntime(input) {
4565
4938
  const restoredSeenExportCount = restoredTeacherState.snapshot.state?.seenExportDigests.length ?? 0;
4566
4939
  log(`Restored teacher snapshot: seen=${restoredSeenExportCount} artifacts=${restoredTeacherState.snapshot.teacher.artifactCount}`);
4567
4940
  }
4568
- const resolvedProfileRoots = input.profileRoots === undefined
4941
+ const resolvedWatchProfileScope = input.profileRoots === undefined
4569
4942
  ? resolveWatchProfileRootsForActivationRoot(activationRoot)
4570
- : [...new Set(input.profileRoots.map((root) => path.resolve(root)))];
4571
- if (input.profileRoots === undefined && resolvedProfileRoots !== undefined) {
4943
+ : {
4944
+ attachedProfileRoots: [...new Set(input.profileRoots.map((root) => path.resolve(root)))],
4945
+ halfAttachedReferences: []
4946
+ };
4947
+ const resolvedProfileRoots = resolvedWatchProfileScope.attachedProfileRoots;
4948
+ if (input.profileRoots === undefined && resolvedProfileRoots !== undefined && resolvedProfileRoots.length > 0) {
4572
4949
  log(`Session tail scope: attached OpenClaw home${resolvedProfileRoots.length === 1 ? "" : "s"} ${resolvedProfileRoots
4573
4950
  .map((root) => shortenPath(root))
4574
4951
  .join(", ")}`);
4575
4952
  }
4953
+ if (input.profileRoots === undefined && resolvedWatchProfileScope.halfAttachedReferences.length > 0) {
4954
+ log(`Session tail scope skipped half-attached OpenClaw home${resolvedWatchProfileScope.halfAttachedReferences.length === 1 ? "" : "s"} ${resolvedWatchProfileScope.halfAttachedReferences
4955
+ .map((reference) => `${shortenPath(path.resolve(reference.openclawHome))} (${summarizeSharedActivationRootReferenceProof(reference)})`)
4956
+ .join(", ")}`);
4957
+ }
4576
4958
  let restoredCursor = loadWatchSessionTailCursor(sessionTailCursorPath);
4577
4959
  let localSessionTail;
4578
4960
  try {
@@ -5097,6 +5479,10 @@ export function runOperatorCli(argv = process.argv.slice(2)) {
5097
5479
  const operatorInput = {
5098
5480
  ...statusOrRollback.input,
5099
5481
  activationRoot,
5482
+ openclawHome: statusOrRollback.openclawHome,
5483
+ ...(targetInspection?.profileId === null || targetInspection?.profileId === undefined
5484
+ ? {}
5485
+ : { profileId: targetInspection.profileId }),
5100
5486
  teacherSnapshotPath: resolveOperatorTeacherSnapshotPath(activationRoot, statusOrRollback.input.teacherSnapshotPath)
5101
5487
  };
5102
5488
  const status = describeCurrentProfileBrainStatus(operatorInput);