@openclawbrain/openclaw 0.3.0 → 0.3.1

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
@@ -1,21 +1,30 @@
1
1
  #!/usr/bin/env node
2
2
  import { execFileSync, execSync } from "node:child_process";
3
- import { existsSync, mkdirSync, readFileSync, readdirSync, readSync, openSync, closeSync, realpathSync, rmSync, statSync, writeFileSync, appendFileSync, symlinkSync } from "node:fs";
3
+ import { existsSync, mkdirSync, readFileSync, readdirSync, readSync, openSync, closeSync, realpathSync, rmSync, statSync, writeFileSync, symlinkSync } from "node:fs";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath, pathToFileURL } from "node:url";
6
6
  const __filename = fileURLToPath(import.meta.url);
7
7
  const __dirname = path.dirname(__filename);
8
+ import { DEFAULT_OLLAMA_EMBEDDING_MODEL } from "@openclawbrain/compiler";
8
9
  import { parseDaemonArgs, runDaemonCommand } from "./daemon.js";
9
10
  import { exportBrain, importBrain } from "./import-export.js";
10
11
  import { buildNormalizedEventExport } from "@openclawbrain/contracts";
11
12
  import { buildTeacherSupervisionArtifactsFromNormalizedEventExport, createAlwaysOnLearningRuntimeState, describeAlwaysOnLearningRuntimeState, drainAlwaysOnLearningRuntime, loadOrInitBaseline, materializeAlwaysOnLearningCandidatePack, persistBaseline } from "@openclawbrain/learner";
12
13
  import { inspectActivationState, loadPackFromActivation, promoteCandidatePack, readLearningSpineLogEntries, stageCandidatePack } from "@openclawbrain/pack-format";
13
14
  import { resolveActivationRoot } from "./resolve-activation-root.js";
14
- import { buildNormalizedEventExportFromScannedEvents, bootstrapRuntimeAttach, buildOperatorSurfaceReport, compileRuntimeContext, createAsyncTeacherLiveLoop, createOpenClawLocalSessionTail, createRuntimeEventExportScanner, describeCurrentProfileBrainStatus, formatBootstrapRuntimeAttachReport, formatOperatorRollbackReport, loadWatchTeacherSnapshotState, loadRuntimeEventExportBundle, persistWatchTeacherSnapshot, rollbackRuntimeAttach, resolveOperatorTeacherSnapshotPath, resolveAsyncTeacherLiveLoopSnapshotPath, resolveWatchSessionTailCursorPath, resolveWatchStateRoot, resolveWatchTeacherSnapshotPath, scanLiveEventExport, scanRecordedSession, summarizeLearningPathFromMaterialization, summarizeNormalizedEventExportLabelFlow, writeScannedEventExportBundle } from "./index.js";
15
+ import { describeOpenClawHomeInspection, discoverOpenClawHomes, formatOpenClawHomeLayout, formatOpenClawHomeProfileSource, inspectOpenClawHome } from "./openclaw-home-layout.js";
16
+ import { 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";
15
17
  import { appendLearningUpdateLogs } from "./learning-spine.js";
16
18
  import { buildPassiveLearningSessionExportFromOpenClawSessionStore } from "./local-session-passive-learning.js";
17
19
  import { discoverOpenClawSessionStores, loadOpenClawSessionIndex, readOpenClawSessionFile } from "./session-store.js";
18
- import { readOpenClawBrainProviderConfig } from "./provider-config.js";
20
+ import { readOpenClawBrainProviderConfig, readOpenClawBrainProviderConfigFromSources, resolveOpenClawBrainProviderDefaultsPath } from "./provider-config.js";
21
+ const OPENCLAWBRAIN_INSTALL_SKIP_EMBEDDER_PROVISION_ENV = "OPENCLAWBRAIN_INSTALL_SKIP_EMBEDDER_PROVISION";
22
+ const INSTALL_COMPATIBLE_LOCAL_TEACHER_MODEL_PREFIXES = [
23
+ "qwen3.5:9b",
24
+ "qwen3.5:8b",
25
+ "qwen3:8b",
26
+ "qwen2.5:7b"
27
+ ];
19
28
  function quoteShellArg(value) {
20
29
  return `'${value.replace(/'/g, `"'"'`)}'`;
21
30
  }
@@ -26,23 +35,18 @@ function normalizeOptionalCliString(value) {
26
35
  const trimmed = value.trim();
27
36
  return trimmed.length > 0 ? trimmed : null;
28
37
  }
38
+ function readTruthyEnvFlag(name, env = process.env) {
39
+ const value = normalizeOptionalCliString(env[name]);
40
+ if (value === null) {
41
+ return false;
42
+ }
43
+ return ["1", "true", "yes", "on"].includes(value.toLowerCase());
44
+ }
29
45
  function getCliHomeDir() {
30
46
  return process.env.HOME ?? process.env.USERPROFILE ?? "~";
31
47
  }
32
48
  function discoverInstallCandidateOpenClawHomes(homeDir = getCliHomeDir()) {
33
- const resolvedHomeDir = path.resolve(homeDir);
34
- let entries;
35
- try {
36
- entries = readdirSync(resolvedHomeDir, { withFileTypes: true });
37
- }
38
- catch {
39
- return [];
40
- }
41
- return entries
42
- .filter((entry) => entry.isDirectory() && entry.name.startsWith(".openclaw-"))
43
- .map((entry) => path.join(resolvedHomeDir, entry.name))
44
- .filter((candidate) => existsSync(path.join(candidate, "openclaw.json")))
45
- .sort((left, right) => left.localeCompare(right));
49
+ return discoverOpenClawHomes(homeDir).map((inspection) => inspection.openclawHome);
46
50
  }
47
51
  function formatInstallOpenClawHomeSource(source) {
48
52
  switch (source) {
@@ -50,8 +54,8 @@ function formatInstallOpenClawHomeSource(source) {
50
54
  return "--openclaw-home";
51
55
  case "env":
52
56
  return "OPENCLAW_HOME";
53
- case "discovered_single_profile":
54
- return "single discovered live profile";
57
+ case "discovered_single_home":
58
+ return "single discovered install target";
55
59
  default:
56
60
  return source;
57
61
  }
@@ -75,24 +79,24 @@ function resolveInstallOpenClawHome(explicitOpenclawHome) {
75
79
  if (discoveredHomes.length === 1) {
76
80
  return {
77
81
  openclawHome: path.resolve(discoveredHomes[0]),
78
- openclawHomeSource: "discovered_single_profile"
82
+ openclawHomeSource: "discovered_single_home"
79
83
  };
80
84
  }
81
85
  if (discoveredHomes.length > 1) {
82
86
  const installPrefix = detectConsumerSafeOperatorCliPrefix();
83
- const targetChoices = discoveredHomes
84
- .map((candidate) => {
85
- const resolvedCandidate = path.resolve(candidate);
86
- return ` - ${resolvedCandidate}\n ${installPrefix} install --openclaw-home ${quoteShellArg(resolvedCandidate)}`;
87
+ const targetChoices = discoverOpenClawHomes()
88
+ .map((inspection) => {
89
+ const resolvedCandidate = path.resolve(inspection.openclawHome);
90
+ return ` - ${resolvedCandidate} (${describeOpenClawHomeInspection(inspection)})\n ${installPrefix} install --openclaw-home ${quoteShellArg(resolvedCandidate)}`;
87
91
  })
88
92
  .join("\n");
89
93
  throw new Error([
90
- "Refusing ambiguous live OpenClaw targets for install.",
94
+ "Refusing ambiguous OpenClaw install targets.",
91
95
  targetChoices,
92
- "Pass --openclaw-home <path> or set OPENCLAW_HOME to pin one profile."
96
+ "Pass --openclaw-home <path> or set OPENCLAW_HOME to pin one OpenClaw home."
93
97
  ].join("\n"));
94
98
  }
95
- throw new Error("No OpenClaw profile home found. Pass --openclaw-home <path> or set OPENCLAW_HOME.");
99
+ throw new Error("No OpenClaw home found. Pass --openclaw-home <path> or set OPENCLAW_HOME.");
96
100
  }
97
101
  function resolveInstallActivationRoot(openclawHome, explicitActivationRoot) {
98
102
  const normalizedExplicitActivationRoot = normalizeOptionalCliString(explicitActivationRoot);
@@ -115,18 +119,24 @@ function resolveInstallWorkspaceId(openclawHome, explicitWorkspaceId) {
115
119
  source: "explicit"
116
120
  };
117
121
  }
118
- try {
119
- const openclawConfigPath = path.join(openclawHome, "openclaw.json");
120
- const openclawConfig = JSON.parse(readFileSync(openclawConfigPath, "utf8"));
121
- if (typeof openclawConfig.profile === "string" && openclawConfig.profile.trim().length > 0) {
122
- return {
123
- workspaceId: openclawConfig.profile.trim(),
124
- source: "openclaw_json_profile"
125
- };
126
- }
122
+ const inspection = inspectOpenClawHome(openclawHome);
123
+ if (inspection.profileId !== null) {
124
+ return {
125
+ workspaceId: inspection.profileId,
126
+ source: inspection.profileSource === "directory_name"
127
+ ? "openclaw_home_dir"
128
+ : inspection.profileSource === "openclaw_json_profile"
129
+ ? "openclaw_json_profile"
130
+ : inspection.profileSource === "openclaw_json_single_profile_key"
131
+ ? "openclaw_json_single_profile_key"
132
+ : "fallback"
133
+ };
127
134
  }
128
- catch {
129
- // Fall back to the profile-home name when install is pointed at an incomplete or not-yet-readable profile.
135
+ if (inspection.layout === "shared_home_profiles_in_config" || inspection.layout === "single_openclaw_home") {
136
+ return {
137
+ workspaceId: "current_profile",
138
+ source: "current_profile_boundary"
139
+ };
130
140
  }
131
141
  const dirName = path.basename(openclawHome);
132
142
  if (dirName === ".openclaw") {
@@ -147,6 +157,24 @@ function resolveInstallWorkspaceId(openclawHome, explicitWorkspaceId) {
147
157
  source: "fallback"
148
158
  };
149
159
  }
160
+ function resolveInstallEmbedderProvisionSkip(explicitSkip) {
161
+ if (explicitSkip) {
162
+ return {
163
+ skipEmbedderProvision: true,
164
+ skipEmbedderProvisionSource: "flag"
165
+ };
166
+ }
167
+ if (readTruthyEnvFlag(OPENCLAWBRAIN_INSTALL_SKIP_EMBEDDER_PROVISION_ENV)) {
168
+ return {
169
+ skipEmbedderProvision: true,
170
+ skipEmbedderProvisionSource: "env"
171
+ };
172
+ }
173
+ return {
174
+ skipEmbedderProvision: false,
175
+ skipEmbedderProvisionSource: null
176
+ };
177
+ }
150
178
  function formatInstallActivationRootSource(source) {
151
179
  if (source === "explicit") {
152
180
  return "explicit --activation-root";
@@ -159,6 +187,10 @@ function formatInstallWorkspaceIdSource(source) {
159
187
  return "explicit --workspace-id";
160
188
  case "openclaw_json_profile":
161
189
  return "from openclaw.json profile";
190
+ case "openclaw_json_single_profile_key":
191
+ return "from the only openclaw.json profiles entry";
192
+ case "current_profile_boundary":
193
+ return "current_profile boundary for a shared OpenClaw home";
162
194
  case "openclaw_home_dir":
163
195
  return "from OpenClaw home dir";
164
196
  default:
@@ -289,29 +321,29 @@ function operatorCliHelp() {
289
321
  return [
290
322
  "Usage:",
291
323
  " openclawbrain install [--openclaw-home <path>] [options]",
324
+ " openclawbrain attach --openclaw-home <path> [options]",
325
+ " openclawbrain <status|rollback> [--activation-root <path>|--openclaw-home <path>] [options]",
326
+ " openclawbrain watch --activation-root <path> [--scan-root <path>] [--interval <seconds>]",
327
+ " openclawbrain daemon <start|stop|status|logs> --activation-root <path> [--json]",
292
328
  " openclawbrain detach --openclaw-home <path> [options]",
293
329
  " openclawbrain uninstall --openclaw-home <path> [--keep-data|--purge-data] [options]",
294
- " openclawbrain attach --activation-root <path> [options]",
295
- " openclawbrain <status|rollback> [--activation-root <path>|--openclaw-home <path>] [options]",
296
330
  " openclawbrain context \"message\" [--activation-root <path>|--openclaw-home <path>]",
297
331
  " openclawbrain history [--activation-root <path>|--openclaw-home <path>] [--limit N] [--json]",
298
332
  " openclawbrain scan --session <trace.json> --root <path> [options]",
299
333
  " openclawbrain scan --live <event-export-path> --workspace <workspace.json> [options]",
300
334
  " openclawbrain learn [--activation-root <path>|--openclaw-home <path>] [--json]",
301
- " openclawbrain watch --activation-root <path> [--scan-root <path>] [--interval <seconds>]",
302
- " openclawbrain daemon <start|stop|status|logs> --activation-root <path> [--json]",
303
335
  " openclawbrain-ops <status|rollback> [--activation-root <path>|--openclaw-home <path>] [options] # compatibility alias",
304
336
  " openclawbrain-ops scan --session <trace.json> --root <path> [options] # compatibility alias",
305
337
  "",
306
338
  "Options:",
307
- " --openclaw-home <path> OpenClaw profile home dir for install/detach/uninstall (e.g. ~/.openclaw-Tern). Also pins status/rollback/context/history/learn to that installed profile when applicable.",
308
- " --shared Set brain-attachment-policy to shared instead of dedicated (install only).",
309
- " --activation-root <path> Explicit activation root for attach/watch/daemon and other stateful commands; install defaults to sibling .openclawbrain/activation next to the selected OpenClaw home.",
339
+ " --openclaw-home <path> OpenClaw home dir for install/attach/detach/uninstall (e.g. ~/.openclaw-Tern or ~/.openclaw). Also pins status/rollback/context/history/learn to that installed target when applicable.",
340
+ " --shared Set brain-attachment-policy to shared instead of dedicated (install/attach only).",
341
+ ` --skip-embedder-provision Skip the default Ollama ${DEFAULT_OLLAMA_EMBEDDING_MODEL} pull before install/attach bootstrap. Use only when intentionally deferring embedder setup. Also supports ${OPENCLAWBRAIN_INSTALL_SKIP_EMBEDDER_PROVISION_ENV}=1.`,
342
+ " --activation-root <path> Explicit activation root for attach/watch/daemon and other stateful commands; install/attach default to sibling .openclawbrain/activation next to the selected OpenClaw home.",
310
343
  " --keep-data Preserve activation data on uninstall; detach always behaves this way.",
311
344
  " --purge-data Remove activation data on uninstall; requires the installed profile hook or --activation-root.",
312
345
  " --restart <never|safe|external> Restart guidance mode for detach/uninstall. 'safe' is conservative; 'never' leaves restart entirely to the operator.",
313
- " --pack-root <path> Initial pack root directory (attach only; defaults to <activation-root>/packs/initial).",
314
- " --workspace-id <id> Workspace identifier for attach/install provenance; install defaults to openclaw.json.profile or the profile name, attach defaults to 'workspace'.",
346
+ " --workspace-id <id> Workspace identifier for install/attach provenance; defaults to the detected profile target from openclaw.json when possible, otherwise the profile name or current_profile boundary.",
315
347
  " --event-export <path> Event-export bundle root or normalized export JSON payload.",
316
348
  " --teacher-snapshot <path> Canonical watch teacher snapshot JSON or raw async teacher snapshot JSON; keeps live-first, principal-priority, and passive-backfill learner truth explicit.",
317
349
  " --updated-at <iso> Observation time to use for freshness checks.",
@@ -321,7 +353,7 @@ function operatorCliHelp() {
321
353
  " --session <path> Sanitized recorded-session trace JSON to replay.",
322
354
  " --live <path> Runtime event-export bundle root or normalized export JSON to scan once.",
323
355
  " --root <path> Output root for scan --session replay artifacts.",
324
- " --workspace <path> Workspace metadata JSON for scan --live candidate provenance.",
356
+ " --workspace <path> Workspace metadata JSON for scan --live candidate materialization.",
325
357
  " --pack-label <label> Candidate-pack label for scan --live. Defaults to scanner-live-cli.",
326
358
  " --observed-at <iso> Observation time for scan --live freshness checks.",
327
359
  " --snapshot-out <path> Write the one-shot scan --live snapshot JSON.",
@@ -331,25 +363,31 @@ function operatorCliHelp() {
331
363
  " --json Emit machine-readable JSON instead of text.",
332
364
  " --help Show this help.",
333
365
  "",
334
- "Common flow:",
335
- " 0. install openclawbrain install — attach the brain with sane defaults; pass --openclaw-home for explicit targeting on many-profile hosts",
336
- " 0. detach openclawbrain detach --openclaw-home <path> — remove the profile hook only; activation data stays in place",
337
- " 0. uninstall openclawbrain uninstall --openclaw-home <path> --keep-data|--purge-data remove the profile hook and choose the data outcome explicitly",
338
- " 0. context openclawbrain context \"hello\" preview the brain context that would be injected for a message",
339
- " 0. attach openclawbrain attach --activation-root <path>",
340
- " 1. status openclawbrain status --activation-root <path> — answer \"How's the brain?\" for that boundary",
341
- " 2. status --json openclawbrain status --activation-root <path> --json read the canonical current_profile_brain_status.v1 object",
342
- " 3. rollback --dry-run openclawbrain rollback --activation-root <path> --dry-run preview active <- previous, active -> candidate",
343
- " 4. rollback openclawbrain rollback --activation-root <path> — apply the rollback when the preview says ready",
344
- " 5. scan --session replay one sanitized session trace across no_brain, seed_pack, and learned_replay",
345
- " 6. scan --live scan one live event export into teacher/learner state without claiming a daemon is running",
366
+ "Lifecycle flow:",
367
+ " 1. install openclawbrain install — safe first-time default; pass --openclaw-home when more than one OpenClaw home/layout is present",
368
+ " 2. attach openclawbrain attach --openclaw-home <path> [--activation-root <path>] explicit reattach/manual hook path for known brain data; use install first",
369
+ " 3. status openclawbrain status --activation-root <path> — answer \"How's the brain?\" for that boundary",
370
+ " 4. status --detailed openclawbrain status --activation-root <path> --detailed explain serve path, freshness, backlog, and failure mode",
371
+ " 5. watch openclawbrain watch --activation-root <path> — run the foreground learning/watch loop",
372
+ " 6. daemon start openclawbrain daemon start --activation-root <path> — keep watch running in the background on macOS",
373
+ " 7. daemon status openclawbrain daemon status --activation-root <path> — inspect the background watch state",
374
+ " 8. detach openclawbrain detach --openclaw-home <path> — remove the profile hookup only and keep brain data",
375
+ " 9. uninstall openclawbrain uninstall --openclaw-home <path> --keep-data|--purge-data remove the hookup and choose the data outcome explicitly",
376
+ "",
377
+ "Advanced/operator surfaces:",
378
+ " context preview the brain context that would be injected for a message",
379
+ " rollback preview or apply active <- previous, active -> candidate pointer movement",
380
+ " scan inspect one recorded session or live event export without claiming a daemon is running",
381
+ " learn one-shot local-session learning pass against the resolved activation root",
346
382
  " status --teacher-snapshot keeps the current live-first / principal-priority / passive-backfill learner order visible when that snapshot exists",
347
383
  " watch/daemon persist their operator snapshot at <activation-root>/watch/teacher-snapshot.json; --teacher-snapshot overrides the default path",
348
- " watch Ollama teacher labels: set OPENCLAWBRAIN_TEACHER_PROVIDER=ollama plus optional OPENCLAWBRAIN_TEACHER_BASE_URL, OPENCLAWBRAIN_TEACHER_MODEL, and OPENCLAWBRAIN_TEACHER_* budget vars",
384
+ " 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",
349
385
  "",
350
386
  "Exit codes:",
387
+ " install: 0 on successful profile hookup/bootstrap, 1 on input/read failure.",
351
388
  " status: 0 on successful inspection, 1 on input/read failure.",
352
389
  " rollback: 0 when ready/applied, 1 when blocked or on input/read failure.",
390
+ " attach: 0 on successful profile hookup/bootstrap, 1 on input/read failure.",
353
391
  " detach: 0 on successful unhook, 1 on input/read failure.",
354
392
  " uninstall: 0 on successful unhook/cleanup, 1 on input/read failure.",
355
393
  " scan: 0 on successful replay/scan, 1 on input/read failure."
@@ -391,7 +429,8 @@ function formatLearningBuckets(report) {
391
429
  return `pi:${buckets.principal_immediate},pb:${buckets.principal_backfill},live:${buckets.live},backfill:${buckets.backfill}`;
392
430
  }
393
431
  function formatLearningWarnings(report) {
394
- return report.learning.warningStates.length === 0 ? "none" : report.learning.warningStates.join("|");
432
+ const warnings = report.learning.warningStates.filter((warning) => warning !== "teacher_snapshot_unavailable");
433
+ return warnings.length === 0 ? "none" : warnings.join("|");
395
434
  }
396
435
  function formatLabelFlowSummary(labelFlow) {
397
436
  return `source=${labelFlow.source} human=${labelFlow.humanLabelCount ?? "none"} self=${labelFlow.selfLabelCount ?? "none"} implicitPositive=${labelFlow.implicitPositiveCount ?? "none"} teacherArtifacts=${labelFlow.asyncTeacherArtifactCount ?? "none"}`;
@@ -399,11 +438,318 @@ function formatLabelFlowSummary(labelFlow) {
399
438
  function formatLearningPathSummary(learningPath) {
400
439
  return `source=${learningPath.source} pg=${learningPath.policyGradientVersion} method=${learningPath.policyGradientMethod ?? "none"} target=${learningPath.targetConstruction ?? "none"} connect=${learningPath.connectOpsFired ?? "none"} trajectories=${learningPath.reconstructedTrajectoryCount ?? "none"}`;
401
440
  }
402
- function formatCurrentProfileStatusSummary(status, report) {
441
+ function formatTeacherLoopSummary(report) {
442
+ const parts = [
443
+ `snapshot=${report.teacherLoop.sourcePath ?? "none"}`,
444
+ `kind=${report.teacherLoop.sourceKind}`,
445
+ `lastRun=${report.teacherLoop.lastRunAt ?? "none"}`,
446
+ `artifacts=${report.teacherLoop.artifactCount ?? "none"}`,
447
+ `freshness=${report.teacherLoop.latestFreshness}`,
448
+ `queue=${report.teacherLoop.queueDepth ?? "none"}/${report.teacherLoop.queueCapacity ?? "none"}`,
449
+ `running=${yesNo(report.teacherLoop.running)}`
450
+ ];
451
+ if (report.teacherLoop.lastNoOpReason !== "none") {
452
+ parts.push(`noOp=${report.teacherLoop.lastNoOpReason}`);
453
+ }
454
+ if (report.teacherLoop.failureMode !== "none") {
455
+ const failureDetail = report.teacherLoop.failureDetail === null
456
+ ? report.teacherLoop.failureMode
457
+ : `${report.teacherLoop.failureMode}(${report.teacherLoop.failureDetail})`;
458
+ parts.push(`failure=${failureDetail}`);
459
+ }
460
+ return parts.join(" ");
461
+ }
462
+ function formatCompactValue(value, maxLength = 64) {
463
+ return value.length <= maxLength ? value : `${value.slice(0, maxLength - 1)}...`;
464
+ }
465
+ function formatCompactList(values, maxItems = 2, maxLength = 64) {
466
+ if (values.length === 0) {
467
+ return "none";
468
+ }
469
+ const visible = values.slice(0, maxItems).map((value) => formatCompactValue(value, maxLength));
470
+ return values.length > maxItems ? `${visible.join("|")}+${values.length - maxItems}more` : visible.join("|");
471
+ }
472
+ const SERVICE_RISK_FINDING_CODES = new Set([
473
+ "activation_broken_install",
474
+ "activation_stale_incomplete",
475
+ "active_missing",
476
+ "active_unhealthy",
477
+ "learned_route_missing",
478
+ "serve_path_fail_open",
479
+ "serve_path_hard_fail",
480
+ "serve_path_route_evidence_missing"
481
+ ]);
482
+ const DEGRADED_BRAIN_FINDING_CODES = new Set([
483
+ "bootstrap_waiting_for_first_export",
484
+ "serve_path_unprobed",
485
+ "brain_context_kernel_only",
486
+ "candidate_unhealthy",
487
+ "promotion_blocked",
488
+ "supervision_not_flowing",
489
+ "scan_surfaces_missing"
490
+ ]);
491
+ const COSMETIC_FINDING_CODES = new Set([
492
+ "last_promotion_unknown",
493
+ "rollback_blocked",
494
+ "supervision_unavailable",
495
+ "turn_attribution_partial",
496
+ "teacher_snapshot_unavailable"
497
+ ]);
498
+ const LEARNING_WARNING_MESSAGES = {
499
+ awaiting_first_export: "awaiting first export",
500
+ principal_live_backlog: "principal live backlog is ahead of serving",
501
+ principal_backfill_pending: "principal backfill is still queued",
502
+ active_pack_behind_latest_principal: "active pack is behind the latest principal correction",
503
+ passive_backfill_pending: "passive backfill remains queued",
504
+ teacher_queue_full: "teacher queue is full",
505
+ teacher_labels_stale: "teacher labels are stale",
506
+ teacher_no_artifacts: "teacher produced no artifacts",
507
+ teacher_snapshot_unavailable: "teacher snapshot is unavailable"
508
+ };
509
+ function summarizeStatusInstallHook(openclawHome) {
510
+ if (openclawHome === null) {
511
+ return {
512
+ state: "unknown",
513
+ detail: "profile hook state is unknown from activation-root-only status; pin --openclaw-home to prove install state"
514
+ };
515
+ }
516
+ const extensionDir = path.join(path.resolve(openclawHome), "extensions", "openclawbrain");
517
+ const indexPath = path.join(extensionDir, "index.ts");
518
+ const runtimeGuardPath = path.join(extensionDir, "runtime-guard.js");
519
+ const manifestPath = path.join(extensionDir, "openclaw.plugin.json");
520
+ if (existsSync(indexPath) && existsSync(runtimeGuardPath) && existsSync(manifestPath)) {
521
+ return {
522
+ state: "installed",
523
+ detail: `profile hook is installed at ${shortenPath(extensionDir)}`
524
+ };
525
+ }
526
+ return {
527
+ state: "not_installed",
528
+ detail: `profile hook is not present at ${shortenPath(extensionDir)}`
529
+ };
530
+ }
531
+ function runOllamaProbe(args, baseUrl) {
532
+ try {
533
+ execFileSync("ollama", [...args], {
534
+ stdio: "pipe",
535
+ timeout: 2_000,
536
+ env: {
537
+ ...process.env,
538
+ OLLAMA_HOST: baseUrl
539
+ }
540
+ });
541
+ return {
542
+ detected: true,
543
+ detail: `ollama responded to ${args.join(" ")} at ${baseUrl}`
544
+ };
545
+ }
546
+ catch (error) {
547
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
548
+ return {
549
+ detected: false,
550
+ detail: "ollama CLI was not found on PATH"
551
+ };
552
+ }
553
+ return {
554
+ detected: true,
555
+ detail: describeExecFailure(error)
556
+ };
557
+ }
558
+ }
559
+ function summarizeStatusEmbeddings(report, providerConfig) {
560
+ let embeddedEntryCount = null;
561
+ let totalEntryCount = null;
562
+ let models = [];
563
+ let liveState = "unknown";
564
+ let liveDetail = "no activation-ready active pack is available for embedding inspection";
565
+ if (report.active !== null && report.active.activationReady) {
566
+ try {
567
+ const activePack = loadPackFromActivation(report.activationRoot, "active", {
568
+ requireActivationReady: true
569
+ });
570
+ if (activePack !== null) {
571
+ totalEntryCount = activePack.vectors.entries.length;
572
+ embeddedEntryCount = activePack.vectors.entries.filter((entry) => entry.embedding !== undefined).length;
573
+ models = [...new Set(activePack.vectors.entries.flatMap((entry) => (entry.embedding === undefined ? [] : [entry.embedding.model])))].sort((left, right) => left.localeCompare(right));
574
+ liveState = embeddedEntryCount > 0 ? "yes" : "no";
575
+ liveDetail = `active pack stores ${embeddedEntryCount}/${totalEntryCount} numeric embeddings`;
576
+ }
577
+ }
578
+ catch (error) {
579
+ liveDetail = `embedding inspection failed: ${toErrorMessage(error)}`;
580
+ }
581
+ }
582
+ if (providerConfig.embedder.provider === "off") {
583
+ return {
584
+ provider: providerConfig.embedder.provider,
585
+ model: providerConfig.embedder.model,
586
+ provisionedState: "off",
587
+ liveState,
588
+ embeddedEntryCount,
589
+ totalEntryCount,
590
+ models,
591
+ detail: `${liveDetail}; embedder provider is off`
592
+ };
593
+ }
594
+ if (providerConfig.embedder.provider === "keywords") {
595
+ return {
596
+ provider: providerConfig.embedder.provider,
597
+ model: providerConfig.embedder.model,
598
+ provisionedState: "builtin",
599
+ liveState,
600
+ embeddedEntryCount,
601
+ totalEntryCount,
602
+ models,
603
+ detail: `${liveDetail}; keyword embedder needs no Ollama model provision`
604
+ };
605
+ }
606
+ const modelProbe = runOllamaProbe(["show", providerConfig.embedder.model], providerConfig.embedderBaseUrl);
607
+ return {
608
+ provider: providerConfig.embedder.provider,
609
+ model: providerConfig.embedder.model,
610
+ provisionedState: modelProbe.detected && /responded to/.test(modelProbe.detail) ? "confirmed" : "not_confirmed",
611
+ liveState,
612
+ embeddedEntryCount,
613
+ totalEntryCount,
614
+ models,
615
+ detail: `${liveDetail}; ollama model check: ${modelProbe.detail}`
616
+ };
617
+ }
618
+ function summarizeStatusLocalLlm(providerConfig) {
619
+ const detection = runOllamaProbe(["--version"], providerConfig.teacherBaseUrl);
620
+ const enabled = providerConfig.teacher.provider === "ollama";
621
+ if (enabled) {
622
+ return {
623
+ detected: detection.detected,
624
+ enabled,
625
+ provider: providerConfig.teacher.provider,
626
+ model: providerConfig.teacher.model,
627
+ detail: detection.detected
628
+ ? `teacher provider is ollama and the local LLM surface answered at ${providerConfig.teacherBaseUrl}`
629
+ : `teacher provider is ollama but the local LLM surface was not detected (${detection.detail})`
630
+ };
631
+ }
632
+ return {
633
+ detected: detection.detected,
634
+ enabled,
635
+ provider: providerConfig.teacher.provider,
636
+ model: providerConfig.teacher.model,
637
+ detail: detection.detected
638
+ ? `local Ollama is detectable, but teacher labeling is ${providerConfig.teacher.provider}`
639
+ : `teacher labeling is ${providerConfig.teacher.provider}; no local Ollama CLI was detected`
640
+ };
641
+ }
642
+ function pushUniqueAlert(target, value) {
643
+ const normalized = value.trim();
644
+ if (normalized.length === 0) {
645
+ return;
646
+ }
647
+ if (target.includes(normalized) === false) {
648
+ target.push(normalized);
649
+ }
650
+ }
651
+ function summarizeStatusAlerts(report, providerConfig, embeddings, localLlm) {
652
+ const buckets = {
653
+ serviceRisk: [],
654
+ degradedBrain: [],
655
+ cosmeticNoise: []
656
+ };
657
+ for (const finding of report.findings) {
658
+ if (finding.severity === "pass") {
659
+ continue;
660
+ }
661
+ if (SERVICE_RISK_FINDING_CODES.has(finding.code)) {
662
+ pushUniqueAlert(buckets.serviceRisk, finding.summary);
663
+ continue;
664
+ }
665
+ if (DEGRADED_BRAIN_FINDING_CODES.has(finding.code)) {
666
+ pushUniqueAlert(buckets.degradedBrain, finding.summary);
667
+ continue;
668
+ }
669
+ if (COSMETIC_FINDING_CODES.has(finding.code)) {
670
+ pushUniqueAlert(buckets.cosmeticNoise, finding.summary);
671
+ continue;
672
+ }
673
+ pushUniqueAlert(finding.severity === "fail" ? buckets.serviceRisk : buckets.degradedBrain, finding.summary);
674
+ }
675
+ for (const warningState of report.learning.warningStates) {
676
+ const message = LEARNING_WARNING_MESSAGES[warningState];
677
+ if (message === undefined) {
678
+ continue;
679
+ }
680
+ if (warningState === "teacher_snapshot_unavailable") {
681
+ pushUniqueAlert(buckets.cosmeticNoise, message);
682
+ }
683
+ else {
684
+ pushUniqueAlert(buckets.degradedBrain, message);
685
+ }
686
+ }
687
+ if (providerConfig.warnings.length > 0) {
688
+ pushUniqueAlert(buckets.cosmeticNoise, "provider env warnings forced fallback defaults");
689
+ }
690
+ if (localLlm.enabled && !localLlm.detected) {
691
+ pushUniqueAlert(buckets.degradedBrain, "local LLM is enabled but not detected");
692
+ }
693
+ if (embeddings.provider === "ollama" && embeddings.provisionedState !== "confirmed") {
694
+ pushUniqueAlert(buckets.degradedBrain, `embedder model ${embeddings.model} is not confirmed on Ollama`);
695
+ }
696
+ if (embeddings.provider === "ollama" && embeddings.liveState === "no") {
697
+ pushUniqueAlert(buckets.degradedBrain, "embedder is provisioned but the active pack has no live numeric embeddings");
698
+ }
699
+ return buckets;
700
+ }
701
+ function summarizeStatusWatchState(report) {
702
+ if (!report.teacherLoop.available || report.teacherLoop.sourceKind !== "watch_snapshot") {
703
+ return "not_visible";
704
+ }
705
+ return report.teacherLoop.running === true ? "running" : "snapshot_only";
706
+ }
707
+ function summarizeStatusServeReality(status) {
708
+ if (status.brainStatus.serveState === "serving_active_pack") {
709
+ return "proven_active_pack";
710
+ }
711
+ return status.brainStatus.serveState;
712
+ }
713
+ function formatStatusAlertLine(values) {
714
+ const normalized = values.map((value) => value.trim()).filter((value) => value.length > 0);
715
+ return normalized.length === 0 ? "none" : formatCompactList(normalized, 2, 64);
716
+ }
717
+ function summarizeStatusStartupToken(status) {
718
+ if (status.attachment.state !== "attached") {
719
+ return "BRAIN_NOT_YET_LOADED";
720
+ }
721
+ if (status.brainStatus.activationState === "broken_install" || status.brainStatus.activationState === "stale_incomplete" || status.brainStatus.activationState === "detached") {
722
+ return "BRAIN_NOT_YET_LOADED";
723
+ }
724
+ return status.brainStatus.serveState === "serving_active_pack" ? "BRAIN_LOADED" : "BRAIN_NOT_YET_LOADED";
725
+ }
726
+ function buildCompactStatusHeader(status, report, options) {
727
+ const installHook = summarizeStatusInstallHook(options.openclawHome);
728
+ const embeddings = summarizeStatusEmbeddings(report, options.providerConfig);
729
+ const localLlm = summarizeStatusLocalLlm(options.providerConfig);
730
+ const alerts = summarizeStatusAlerts(report, options.providerConfig, embeddings, localLlm);
731
+ const promoted = status.brain.state === "pg_promoted_pack_authoritative" ? "yes" : "no";
732
+ const liveModels = embeddings.models.length === 0 ? "none" : embeddings.models.join("|");
733
+ return [
734
+ `reality hook=${installHook.state} attach=${status.attachment.state} watch=${summarizeStatusWatchState(report)} promoted=${promoted} serve=${summarizeStatusServeReality(status)}`,
735
+ `startup ${summarizeStatusStartupToken(status)} init=${status.brainStatus.activationState} proof=status_probe`,
736
+ `explain ${status.brain.summary}`,
737
+ `embeddings provider=${embeddings.provider} provisioned=${embeddings.provisionedState} live=${embeddings.liveState} stored=${embeddings.embeddedEntryCount ?? "none"}/${embeddings.totalEntryCount ?? "none"} models=${liveModels}`,
738
+ `localLLM detected=${yesNo(localLlm.detected)} enabled=${yesNo(localLlm.enabled)} provider=${localLlm.provider} model=${localLlm.model}`,
739
+ `alerts service_risk=${formatStatusAlertLine(alerts.serviceRisk)} degraded_brain=${formatStatusAlertLine(alerts.degradedBrain)} cosmetic_noise=${formatStatusAlertLine(alerts.cosmeticNoise)}`
740
+ ];
741
+ }
742
+ function formatCurrentProfileStatusSummary(status, report, targetInspection, options) {
403
743
  const profileIdSuffix = status.profile.profileId === null ? "" : ` id=${status.profile.profileId}`;
744
+ const targetLine = targetInspection === null
745
+ ? `target activation=${status.host.activationRoot} source=activation_root_only`
746
+ : `target activation=${status.host.activationRoot} ${formatOpenClawTargetLine(targetInspection)} hook=${shortenPath(path.join(targetInspection.openclawHome, "extensions", "openclawbrain", "index.ts"))}`;
404
747
  return [
405
748
  `STATUS ${status.brainStatus.status}`,
749
+ ...buildCompactStatusHeader(status, report, options),
406
750
  `answer ${status.brain.summary}`,
751
+ targetLine,
752
+ ...(targetInspection === null ? [] : [`preflight ${formatOpenClawTargetExplanation(targetInspection)}`]),
407
753
  `host runtime=${status.host.runtimeOwner} activation=${status.host.activationRoot}`,
408
754
  `profile selector=${status.profile.selector}${profileIdSuffix} attachment=${status.attachment.state} policy=${status.attachment.policyMode}`,
409
755
  `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)}`,
@@ -419,7 +765,7 @@ function formatCurrentProfileStatusSummary(status, report) {
419
765
  `graph source=${report.graph.runtimePlasticitySource ?? "none"} ops=${formatStructuralOps(report)} changed=${yesNo(report.graph.changed)} pruned=${report.graph.prunedBlockCount ?? "none"} strongest=${report.graph.strongestBlockId ?? "none"} summary=${report.graph.operatorSummary ?? report.graph.detail}`,
420
766
  `path ${formatLearningPathSummary(report.learningPath)}`,
421
767
  `learning state=${report.learning.backlogState} bootstrapped=${yesNo(report.learning.bootstrapped)} mode=${report.learning.mode} next=${report.learning.nextPriorityLane} priority=${report.learning.nextPriorityBucket} pending=${report.learning.pendingLive ?? "none"}/${report.learning.pendingBackfill ?? "none"} buckets=${formatLearningBuckets(report)} warn=${formatLearningWarnings(report)} lastPack=${report.learning.lastMaterializedPackId ?? "none"} detail=${report.learning.detail}`,
422
- `teacher snapshot=${report.teacherLoop.sourcePath ?? "none"} kind=${report.teacherLoop.sourceKind} lastRun=${report.teacherLoop.lastRunAt ?? "none"} artifacts=${report.teacherLoop.artifactCount ?? "none"} freshness=${report.teacherLoop.latestFreshness} queue=${report.teacherLoop.queueDepth ?? "none"}/${report.teacherLoop.queueCapacity ?? "none"} running=${yesNo(report.teacherLoop.running)} noOp=${report.teacherLoop.lastNoOpReason} failure=${report.teacherLoop.failureMode}${report.teacherLoop.failureDetail === null ? "" : `(${report.teacherLoop.failureDetail})`}`,
768
+ `teacher ${formatTeacherLoopSummary(report)}`,
423
769
  `passive cadence=${report.teacherLoop.learningCadence} scan=${report.teacherLoop.scanPolicy} slices=${report.teacherLoop.liveSlicesPerCycle ?? "none"}/${report.teacherLoop.backfillSlicesPerCycle ?? "none"} replayed=${report.teacherLoop.replayedBundleCount ?? "none"}/${report.teacherLoop.replayedEventCount ?? "none"} exported=${report.teacherLoop.exportedBundleCount ?? "none"}/${report.teacherLoop.exportedEventCount ?? "none"} tail=${report.teacherLoop.sessionTailSessionsTracked ?? "none"}/${report.teacherLoop.sessionTailBridgedEventCount ?? "none"} tailState=${report.teacherLoop.localSessionTailNoopReason ?? "none"} lastJob=${report.teacherLoop.lastAppliedMaterializationJobId ?? "none"} lastPack=${report.teacherLoop.lastMaterializedPackId ?? "none"}`,
424
770
  `rollback ready=${yesNo(report.rollback.allowed)} state=${report.rollback.state} previous=${report.rollback.previousPackId ?? "none"}`,
425
771
  `proof lastExport=${status.brain.lastExportAt ?? "none"} lastLearningUpdate=${status.brain.lastLearningUpdateAt ?? "none"} lastPromotion=${status.brain.lastPromotionAt ?? "none"}`,
@@ -437,15 +783,322 @@ function shortenPath(fullPath) {
437
783
  }
438
784
  return fullPath;
439
785
  }
786
+ function formatOpenClawTargetLine(inspection) {
787
+ const profilePart = inspection.profileId === null
788
+ ? "profile=current_profile"
789
+ : `profile=${inspection.profileId} via ${formatOpenClawHomeProfileSource(inspection.profileSource)}`;
790
+ return `home=${shortenPath(inspection.openclawHome)} layout=${formatOpenClawHomeLayout(inspection.layout)} ${profilePart}`;
791
+ }
792
+ function formatOpenClawTargetExplanation(inspection) {
793
+ return describeOpenClawHomeInspection(inspection);
794
+ }
440
795
  function buildInstallStatusCommand(activationRoot) {
441
796
  return `openclawbrain status --activation-root ${quoteShellArg(activationRoot)}`;
442
797
  }
443
798
  function buildInstallCommand(openclawHome) {
444
799
  return `openclawbrain install --openclaw-home ${quoteShellArg(openclawHome)}`;
445
800
  }
801
+ function buildAttachCommand(openclawHome, activationRoot = null) {
802
+ const parts = ["openclawbrain", "attach", "--openclaw-home", quoteShellArg(openclawHome)];
803
+ if (activationRoot !== null) {
804
+ parts.push("--activation-root", quoteShellArg(activationRoot));
805
+ }
806
+ return parts.join(" ");
807
+ }
808
+ function buildInstallEmbedderProvisionCommand(baseUrl, model) {
809
+ return `OLLAMA_HOST=${quoteShellArg(baseUrl)} ollama pull ${quoteShellArg(model)}`;
810
+ }
811
+ function describeExecOutput(value) {
812
+ if (typeof value === "string") {
813
+ const normalized = value.trim();
814
+ return normalized.length > 0 ? normalized : null;
815
+ }
816
+ if (value instanceof Buffer) {
817
+ const normalized = value.toString("utf8").trim();
818
+ return normalized.length > 0 ? normalized : null;
819
+ }
820
+ return null;
821
+ }
822
+ function describeExecFailure(error) {
823
+ if (error instanceof Error) {
824
+ const childError = error;
825
+ if (childError.code === "ENOENT") {
826
+ return "ollama was not found on PATH";
827
+ }
828
+ const stderr = describeExecOutput(childError.stderr);
829
+ if (stderr !== null) {
830
+ return stderr;
831
+ }
832
+ const stdout = describeExecOutput(childError.stdout);
833
+ if (stdout !== null) {
834
+ return stdout;
835
+ }
836
+ const message = childError.message.trim();
837
+ if (message.length > 0) {
838
+ return message;
839
+ }
840
+ }
841
+ return String(error);
842
+ }
843
+ function toErrorMessage(error) {
844
+ return error instanceof Error ? error.message : String(error);
845
+ }
846
+ function ensureInstallEmbedderReady(parsed) {
847
+ const providerConfig = readOpenClawBrainProviderConfig(process.env);
848
+ const model = DEFAULT_OLLAMA_EMBEDDING_MODEL;
849
+ const baseUrl = providerConfig.embedderBaseUrl;
850
+ if (parsed.skipEmbedderProvision) {
851
+ const skipReason = parsed.skipEmbedderProvisionSource === "flag"
852
+ ? "--skip-embedder-provision"
853
+ : `${OPENCLAWBRAIN_INSTALL_SKIP_EMBEDDER_PROVISION_ENV}=1`;
854
+ return {
855
+ state: "skipped",
856
+ model,
857
+ baseUrl,
858
+ detail: `Skipped default embedder provisioning (${skipReason}); ${parsed.command} continued only because the operator explicitly opted out. ` +
859
+ `Provision it later with ${buildInstallEmbedderProvisionCommand(baseUrl, model)}.`
860
+ };
861
+ }
862
+ try {
863
+ execFileSync("ollama", ["pull", model], {
864
+ stdio: "pipe",
865
+ env: {
866
+ ...process.env,
867
+ OLLAMA_HOST: baseUrl
868
+ }
869
+ });
870
+ }
871
+ catch (error) {
872
+ const detail = describeExecFailure(error);
873
+ throw new Error(`Default embedder provisioning failed before brain init. Tried ${buildInstallEmbedderProvisionCommand(baseUrl, model)}. ` +
874
+ `${parsed.command === "install" ? "Install" : "Attach"} stops here so the bootstrap path does not quietly continue without ${model}. ` +
875
+ `Fix Ollama and rerun ${parsed.command}, or explicitly skip with --skip-embedder-provision or ${OPENCLAWBRAIN_INSTALL_SKIP_EMBEDDER_PROVISION_ENV}=1. ` +
876
+ `Detail: ${detail}`);
877
+ }
878
+ return {
879
+ state: "ensured",
880
+ model,
881
+ baseUrl,
882
+ detail: `Ensured default embedder before brain bootstrap: ${buildInstallEmbedderProvisionCommand(baseUrl, model)}`
883
+ };
884
+ }
885
+ function parseOllamaListModelNames(output) {
886
+ return output
887
+ .split(/\r?\n/u)
888
+ .map((line) => line.trim())
889
+ .filter((line) => line.length > 0 && !/^name\s+/iu.test(line))
890
+ .map((line) => line.split(/\s+/u)[0] ?? "")
891
+ .filter((name) => name.length > 0);
892
+ }
893
+ function selectCompatibleLocalTeacherModel(models) {
894
+ const normalized = models.map((model) => model.trim()).filter((model) => model.length > 0);
895
+ for (const prefix of INSTALL_COMPATIBLE_LOCAL_TEACHER_MODEL_PREFIXES) {
896
+ const exact = normalized.find((model) => model === prefix);
897
+ if (exact !== undefined) {
898
+ return exact;
899
+ }
900
+ const variant = normalized.find((model) => model.startsWith(`${prefix}-`) ||
901
+ model.startsWith(`${prefix}_`) ||
902
+ model.startsWith(`${prefix}.`));
903
+ if (variant !== undefined) {
904
+ return variant;
905
+ }
906
+ }
907
+ return null;
908
+ }
909
+ function detectInstallTeacherDefaults(baseUrl) {
910
+ try {
911
+ const output = execFileSync("ollama", ["list"], {
912
+ stdio: "pipe",
913
+ env: {
914
+ ...process.env,
915
+ OLLAMA_HOST: baseUrl
916
+ }
917
+ }).toString("utf8");
918
+ const availableModels = parseOllamaListModelNames(output);
919
+ const model = selectCompatibleLocalTeacherModel(availableModels);
920
+ if (model === null) {
921
+ return {
922
+ provider: "heuristic",
923
+ model: null,
924
+ baseUrl,
925
+ availableModels,
926
+ detectionDetail: availableModels.length === 0
927
+ ? `No compatible local Ollama teacher model detected on ${baseUrl}; watch keeps heuristic teacher defaults.`
928
+ : `No compatible local Ollama teacher model detected on ${baseUrl}; saw ${availableModels.join(", ")} and kept heuristic teacher defaults.`
929
+ };
930
+ }
931
+ return {
932
+ provider: "ollama",
933
+ model,
934
+ baseUrl,
935
+ availableModels,
936
+ detectionDetail: `Detected compatible local Ollama teacher model ${model} on ${baseUrl}; watch will enable it by default from the installed activation root.`
937
+ };
938
+ }
939
+ catch (error) {
940
+ const detail = describeExecFailure(error);
941
+ return {
942
+ provider: "heuristic",
943
+ model: null,
944
+ baseUrl,
945
+ availableModels: [],
946
+ detectionDetail: `Local Ollama teacher autodetect failed on ${baseUrl}; kept heuristic teacher defaults. Detail: ${detail}`
947
+ };
948
+ }
949
+ }
950
+ function writeInstallProviderDefaults(parsed) {
951
+ const providerConfig = readOpenClawBrainProviderConfig(process.env);
952
+ const teacherDetection = detectInstallTeacherDefaults(providerConfig.teacherBaseUrl);
953
+ const defaultsPath = resolveOpenClawBrainProviderDefaultsPath(parsed.activationRoot);
954
+ const defaults = {
955
+ contract: "openclawbrain_provider_defaults.v1",
956
+ writtenAt: new Date().toISOString(),
957
+ source: "install",
958
+ teacherBaseUrl: providerConfig.teacherBaseUrl,
959
+ embedderBaseUrl: providerConfig.embedderBaseUrl,
960
+ teacher: {
961
+ provider: teacherDetection.provider,
962
+ model: teacherDetection.model,
963
+ detectedLocally: teacherDetection.provider === "ollama",
964
+ detectedFromModel: teacherDetection.model
965
+ },
966
+ embedder: {
967
+ provider: "ollama",
968
+ model: DEFAULT_OLLAMA_EMBEDDING_MODEL
969
+ }
970
+ };
971
+ writeFileSync(defaultsPath, JSON.stringify(defaults, null, 2) + "\n", "utf8");
972
+ return {
973
+ path: defaultsPath,
974
+ defaults,
975
+ detail: `Wrote local provider defaults: ${teacherDetection.detectionDetail}`,
976
+ lifecycleSummary: teacherDetection.provider === "ollama" && teacherDetection.model !== null
977
+ ? `Teacher: auto-enabled local Ollama model ${teacherDetection.model} from install-written defaults`
978
+ : "Teacher: no compatible local Ollama model detected; watch stays heuristic unless explicitly overridden"
979
+ };
980
+ }
981
+ function buildInstallBrainFeedbackSummary(input) {
982
+ const providerDefaultsPath = resolveOpenClawBrainProviderDefaultsPath(input.parsed.activationRoot);
983
+ const embedderState = input.embedderProvision === null ? "unchanged" : input.embedderProvision.state;
984
+ const teacherDefaults = input.providerDefaults?.defaults.teacher;
985
+ const teacherProvider = teacherDefaults?.provider ?? "unknown";
986
+ const teacherModel = teacherDefaults?.model ?? null;
987
+ const detectedLocalLlm = teacherDefaults?.detectedLocally ?? null;
988
+ const provedNow = input.activationPlan.action === "bootstrap"
989
+ ? `hook written, activation root ready, seed/current-profile attach bootstrapped, provider defaults ${input.providerDefaults === null ? "kept" : "written"}`
990
+ : `hook written, activation root kept, active pack ${input.activationPlan.activePackId ?? "unknown"} preserved${input.providerDefaults === null ? "" : ", provider defaults written"}`;
991
+ const notYetProved = input.activationPlan.action === "bootstrap"
992
+ ? `OpenClaw has not reloaded this hook yet; restart plus status still must prove live startup/load and the first exported turn`
993
+ : `OpenClaw has not reloaded this hook yet; this ${input.parsed.command} run does not itself prove live startup/load after restart`;
994
+ return {
995
+ hookPath: input.extensionDir,
996
+ providerDefaultsPath,
997
+ embedder: {
998
+ provider: "ollama",
999
+ model: DEFAULT_OLLAMA_EMBEDDING_MODEL,
1000
+ state: embedderState
1001
+ },
1002
+ teacher: {
1003
+ provider: teacherProvider,
1004
+ model: teacherModel,
1005
+ detectedLocalLlm
1006
+ },
1007
+ startup: {
1008
+ token: "BRAIN_NOT_YET_LOADED",
1009
+ proof: "restart_required"
1010
+ },
1011
+ provedNow,
1012
+ notYetProved,
1013
+ lines: [
1014
+ `target ${formatOpenClawTargetLine(input.targetInspection)} source=${formatInstallOpenClawHomeSource(input.parsed.openclawHomeSource)}`,
1015
+ `hook written=${shortenPath(input.extensionDir)}`,
1016
+ `activation root=${shortenPath(input.parsed.activationRoot)} source=${formatInstallActivationRootSource(input.parsed.activationRootSource)}`,
1017
+ `defaults provider-defaults=${shortenPath(providerDefaultsPath)} state=${input.providerDefaults === null ? "unchanged" : "written"}`,
1018
+ `embedder provider=ollama model=${DEFAULT_OLLAMA_EMBEDDING_MODEL} state=${embedderState}`,
1019
+ `teacher provider=${teacherProvider} model=${teacherModel ?? "none"} localLLM=${detectedLocalLlm === null ? "unknown" : yesNo(detectedLocalLlm)}`,
1020
+ "startup BRAIN_NOT_YET_LOADED proof=restart_required",
1021
+ `provedNow ${provedNow}`,
1022
+ `notYet ${notYetProved}`
1023
+ ]
1024
+ };
1025
+ }
446
1026
  function buildInstallReloadGuidance() {
447
1027
  return "If this OpenClaw profile is currently running, restart it before expecting the new brain hook to load. If it is stopped, the next launch will pick it up.";
448
1028
  }
1029
+ const LEGACY_PROFILE_NOTE_FILENAMES = ["BRAIN.md", "brain.md"];
1030
+ const LEGACY_BRAIN_AGENTS_LINE = "5. Read `BRAIN.md` — your learning brain context";
1031
+ function isLegacyBrainAdvisoryContent(content) {
1032
+ return content.includes("## OpenClawBrain")
1033
+ && content.includes("You have a learning brain attached at ")
1034
+ && content.includes("openclawbrain status --activation-root")
1035
+ && content.includes("openclawbrain rollback --activation-root");
1036
+ }
1037
+ function writeUpdatedTextFile(filePath, nextText, previousText) {
1038
+ const normalizedNextText = previousText.endsWith("\n") ? `${nextText}\n` : nextText;
1039
+ writeFileSync(filePath, normalizedNextText, "utf8");
1040
+ }
1041
+ function collectProfileResidueDirs(openclawHome) {
1042
+ const directories = [path.resolve(openclawHome)];
1043
+ try {
1044
+ const entries = readdirSync(openclawHome, { withFileTypes: true });
1045
+ for (const entry of entries) {
1046
+ if (entry.isDirectory() && entry.name.startsWith("workspace-")) {
1047
+ directories.push(path.join(openclawHome, entry.name));
1048
+ }
1049
+ }
1050
+ }
1051
+ catch {
1052
+ // Residue cleanup stays best-effort.
1053
+ }
1054
+ return directories;
1055
+ }
1056
+ function removeLegacyProfileResidue(openclawHome) {
1057
+ const removedNotes = [];
1058
+ const updatedAgents = [];
1059
+ for (const directory of collectProfileResidueDirs(openclawHome)) {
1060
+ for (const fileName of LEGACY_PROFILE_NOTE_FILENAMES) {
1061
+ const notePath = path.join(directory, fileName);
1062
+ if (!existsSync(notePath)) {
1063
+ continue;
1064
+ }
1065
+ try {
1066
+ const content = readFileSync(notePath, "utf8");
1067
+ if (!isLegacyBrainAdvisoryContent(content)) {
1068
+ continue;
1069
+ }
1070
+ }
1071
+ catch {
1072
+ continue;
1073
+ }
1074
+ rmSync(notePath, { force: true });
1075
+ removedNotes.push(notePath);
1076
+ }
1077
+ const agentsPath = path.join(directory, "AGENTS.md");
1078
+ if (!existsSync(agentsPath)) {
1079
+ continue;
1080
+ }
1081
+ let agentsContent;
1082
+ try {
1083
+ agentsContent = readFileSync(agentsPath, "utf8");
1084
+ }
1085
+ catch {
1086
+ continue;
1087
+ }
1088
+ const nextContent = agentsContent
1089
+ .split("\n")
1090
+ .filter((line) => line.trim() !== LEGACY_BRAIN_AGENTS_LINE)
1091
+ .join("\n");
1092
+ if (nextContent !== agentsContent) {
1093
+ writeUpdatedTextFile(agentsPath, nextContent, agentsContent);
1094
+ updatedAgents.push(agentsPath);
1095
+ }
1096
+ }
1097
+ return {
1098
+ removedNotes,
1099
+ updatedAgents
1100
+ };
1101
+ }
449
1102
  function buildCleanupRestartGuidance(restart) {
450
1103
  if (restart === "never") {
451
1104
  return "No restart requested. If this OpenClaw profile is currently running, it may keep the previous hook state until the next restart.";
@@ -478,43 +1131,16 @@ function buildStatusNextStep(status, report) {
478
1131
  }
479
1132
  return `Use \`openclawbrain status --activation-root ${activationRootArg} --detailed\` when you need the full lifecycle, serve-path, and backlog proof.`;
480
1133
  }
481
- function formatHumanFriendlyStatus(status, report) {
482
- // Brain status line
483
- const brainActive = status.brainStatus.status === "ok" || status.brainStatus.serveState === "serving_active_pack";
484
- const brainIcon = brainActive ? "Active ✓" : status.brainStatus.status === "fail" ? "Inactive ✗" : `${status.brainStatus.status}`;
485
- // Pack line
486
- const packId = status.brain.activePackId ?? "none";
487
- const packShort = packId.length > 9 ? packId.slice(0, 9) : packId;
488
- const state = status.brain.state ?? "unknown";
489
- // Activation root
490
- const activationPath = shortenPath(status.host.activationRoot);
491
- // Policy
492
- const policy = status.attachment.policyMode ?? report.manyProfile.declaredAttachmentPolicy ?? "undeclared";
493
- const principalFrontier = formatPrincipalCheckpointFrontier(report);
494
- const pendingLive = String(report.learning.pendingLive ?? "none");
495
- const pendingBackfill = String(report.learning.pendingBackfill ?? "none");
496
- const nextLane = report.learning.nextPriorityLane ?? "none";
497
- const nextBucket = report.learning.nextPriorityBucket ?? "none";
1134
+ function formatHumanFriendlyStatus(status, report, targetInspection, options) {
498
1135
  const lines = [
499
- `Brain: ${brainIcon}`,
500
- `Pack: ${packShort} (${state})`,
501
- `Activation: ${activationPath}`,
502
- `Policy: ${policy}`,
503
- `Lifecycle: activation=${status.brainStatus.activationState} attachment=${status.attachment.state} serve=${status.brainStatus.serveState} awaitingFirstExport=${yesNo(status.brainStatus.awaitingFirstExport)}`,
504
- `Rollback: state=${report.rollback.state} ready=${yesNo(report.rollback.allowed)} previous=${report.rollback.previousPackId ?? "none"}`,
505
- `Backlog: principal=${principalFrontier} live=${pendingLive} backfill=${pendingBackfill} next=${nextLane}/${nextBucket}`,
506
- `Labels: ${formatLabelFlowSummary(report.labelFlow)}`,
507
- `Teacher: lastRun=${report.teacherLoop.lastRunAt ?? "none"} artifacts=${report.teacherLoop.artifactCount ?? "none"} exported=${report.teacherLoop.exportedBundleCount ?? "none"}/${report.teacherLoop.exportedEventCount ?? "none"} cadence=${report.teacherLoop.learningCadence}/${report.teacherLoop.scanPolicy} failure=${report.teacherLoop.failureMode}`,
508
- `Learning: ${formatLearningPathSummary(report.learningPath)}`
1136
+ `STATUS ${status.brainStatus.status}`,
1137
+ ...buildCompactStatusHeader(status, report, options),
1138
+ ...(targetInspection === null ? [] : [
1139
+ `target ${formatOpenClawTargetLine(targetInspection)}`,
1140
+ `preflight ${formatOpenClawTargetExplanation(targetInspection)}`
1141
+ ]),
1142
+ `next ${buildStatusNextStep(status, report)}`
509
1143
  ];
510
- // Add learning/serve warnings if relevant
511
- if (report.learning.warningStates.length > 0) {
512
- lines.push(`Warnings: ${report.learning.warningStates.join(", ")}`);
513
- }
514
- if (status.brainStatus.awaitingFirstExport) {
515
- lines.push(`Note: Awaiting first event export`);
516
- }
517
- lines.push(`Next: ${buildStatusNextStep(status, report)}`);
518
1144
  return lines.join("\n");
519
1145
  }
520
1146
  function requireActivationRoot(input, openclawHome, command) {
@@ -560,10 +1186,18 @@ function formatScanSessionSummary(result) {
560
1186
  function formatScanLiveSummary(result, snapshotOutPath) {
561
1187
  const materializedPackId = result.snapshot.learner.lastMaterialization?.candidate.summary.packId ?? "none";
562
1188
  const materializationReason = result.snapshot.learner.lastMaterialization?.reason ?? "none";
1189
+ const teacherSummary = [
1190
+ `artifacts=${result.snapshot.teacher.artifactCount}`,
1191
+ `freshness=${result.snapshot.teacher.latestFreshness}`,
1192
+ `humanLabels=${result.supervision.humanLabelCount}`
1193
+ ];
1194
+ if (result.snapshot.diagnostics.lastNoOpReason !== "none") {
1195
+ teacherSummary.push(`noop=${result.snapshot.diagnostics.lastNoOpReason}`);
1196
+ }
563
1197
  return [
564
1198
  "SCAN live ok",
565
1199
  `source digest=${result.supervision.exportDigest} session=${result.supervision.sessionId ?? "none"} channel=${result.supervision.channel ?? "none"} range=${result.supervision.eventRange.start}-${result.supervision.eventRange.end}/${result.supervision.eventRange.count}`,
566
- `teacher artifacts=${result.snapshot.teacher.artifactCount} freshness=${result.snapshot.teacher.latestFreshness} humanLabels=${result.supervision.humanLabelCount} noop=${result.snapshot.diagnostics.lastNoOpReason}`,
1200
+ `teacher ${teacherSummary.join(" ")}`,
567
1201
  `labels source=${result.labelFlow.source} human=${result.labelFlow.humanLabelCount ?? "none"} self=${result.labelFlow.selfLabelCount ?? "none"} implicitPositive=${result.labelFlow.implicitPositiveCount ?? "none"} teacherArtifacts=${result.labelFlow.asyncTeacherArtifactCount ?? "none"}`,
568
1202
  `path source=${result.learningPath.source} pg=${result.learningPath.policyGradientVersion} method=${result.learningPath.policyGradientMethod ?? "none"} target=${result.learningPath.targetConstruction ?? "none"} connect=${result.learningPath.connectOpsFired ?? "none"} trajectories=${result.learningPath.reconstructedTrajectoryCount ?? "none"}`,
569
1203
  `learner packLabel=${result.packLabel} materialized=${materializedPackId} reason=${materializationReason}`,
@@ -583,12 +1217,12 @@ export function parseOperatorCliArgs(argv) {
583
1217
  let rootDir = null;
584
1218
  let workspacePath = null;
585
1219
  let packLabel = null;
586
- let packRoot = null;
587
1220
  let workspaceId = null;
588
1221
  let observedAt = null;
589
1222
  let snapshotOutPath = null;
590
1223
  let openclawHome = null;
591
1224
  let shared = false;
1225
+ let skipEmbedderProvision = false;
592
1226
  let keepData = false;
593
1227
  let purgeData = false;
594
1228
  let restart = "safe";
@@ -992,6 +1626,10 @@ export function parseOperatorCliArgs(argv) {
992
1626
  shared = true;
993
1627
  continue;
994
1628
  }
1629
+ if (arg === "--skip-embedder-provision") {
1630
+ skipEmbedderProvision = true;
1631
+ continue;
1632
+ }
995
1633
  if (arg === "--keep-data") {
996
1634
  keepData = true;
997
1635
  continue;
@@ -1125,14 +1763,6 @@ export function parseOperatorCliArgs(argv) {
1125
1763
  index += 1;
1126
1764
  continue;
1127
1765
  }
1128
- if (arg === "--pack-root") {
1129
- if (next === undefined) {
1130
- throw new Error("--pack-root requires a value");
1131
- }
1132
- packRoot = next;
1133
- index += 1;
1134
- continue;
1135
- }
1136
1766
  if (arg === "--workspace-id") {
1137
1767
  if (next === undefined) {
1138
1768
  throw new Error("--workspace-id requires a value");
@@ -1146,12 +1776,27 @@ export function parseOperatorCliArgs(argv) {
1146
1776
  if (command !== "detach" && command !== "uninstall" && restartExplicitlySet) {
1147
1777
  throw new Error("--restart only applies to detach/uninstall");
1148
1778
  }
1779
+ if (command !== "install" && command !== "attach" && shared) {
1780
+ throw new Error("--shared only applies to install/attach");
1781
+ }
1782
+ if (command !== "install" && command !== "attach" && skipEmbedderProvision) {
1783
+ throw new Error("--skip-embedder-provision only applies to install/attach");
1784
+ }
1149
1785
  if (command !== "uninstall" && keepData) {
1150
1786
  throw new Error("--keep-data only applies to uninstall; use detach to preserve activation data");
1151
1787
  }
1152
1788
  if (command !== "uninstall" && purgeData) {
1153
1789
  throw new Error("--purge-data only applies to uninstall");
1154
1790
  }
1791
+ if (command !== "install" && command !== "attach" && workspaceId !== null) {
1792
+ throw new Error("--workspace-id only applies to install/attach");
1793
+ }
1794
+ if (command !== "scan" && packLabel !== null) {
1795
+ throw new Error("--pack-label only applies to scan --live");
1796
+ }
1797
+ if ((command === "install" || command === "attach") && brainAttachmentPolicy !== null) {
1798
+ throw new Error(`${command} uses dedicated by default or --shared for shared mode; --brain-attachment-policy only applies to status/rollback inspection`);
1799
+ }
1155
1800
  if (command === "install") {
1156
1801
  if (help) {
1157
1802
  return {
@@ -1163,6 +1808,8 @@ export function parseOperatorCliArgs(argv) {
1163
1808
  shared: false,
1164
1809
  workspaceId: "",
1165
1810
  workspaceIdSource: "explicit",
1811
+ skipEmbedderProvision: false,
1812
+ skipEmbedderProvisionSource: null,
1166
1813
  json,
1167
1814
  help
1168
1815
  };
@@ -1170,6 +1817,7 @@ export function parseOperatorCliArgs(argv) {
1170
1817
  const resolvedOpenclawHome = resolveInstallOpenClawHome(openclawHome);
1171
1818
  const resolvedActivationRoot = resolveInstallActivationRoot(resolvedOpenclawHome.openclawHome, activationRoot);
1172
1819
  const resolvedWorkspaceId = resolveInstallWorkspaceId(resolvedOpenclawHome.openclawHome, workspaceId);
1820
+ const resolvedEmbedderProvisionSkip = resolveInstallEmbedderProvisionSkip(skipEmbedderProvision);
1173
1821
  return {
1174
1822
  command,
1175
1823
  openclawHome: resolvedOpenclawHome.openclawHome,
@@ -1179,6 +1827,47 @@ export function parseOperatorCliArgs(argv) {
1179
1827
  shared,
1180
1828
  workspaceId: resolvedWorkspaceId.workspaceId,
1181
1829
  workspaceIdSource: resolvedWorkspaceId.source,
1830
+ skipEmbedderProvision: resolvedEmbedderProvisionSkip.skipEmbedderProvision,
1831
+ skipEmbedderProvisionSource: resolvedEmbedderProvisionSkip.skipEmbedderProvisionSource,
1832
+ json,
1833
+ help
1834
+ };
1835
+ }
1836
+ if (command === "attach") {
1837
+ if (help) {
1838
+ return {
1839
+ command,
1840
+ openclawHome: "",
1841
+ openclawHomeSource: "explicit",
1842
+ activationRoot: "",
1843
+ activationRootSource: "explicit",
1844
+ shared: false,
1845
+ workspaceId: "",
1846
+ workspaceIdSource: "explicit",
1847
+ skipEmbedderProvision: false,
1848
+ skipEmbedderProvisionSource: null,
1849
+ json,
1850
+ help
1851
+ };
1852
+ }
1853
+ if (openclawHome === null || openclawHome.trim().length === 0) {
1854
+ throw new Error("--openclaw-home is required for attach; use install for the first-time default path");
1855
+ }
1856
+ const resolvedOpenclawHome = path.resolve(openclawHome);
1857
+ const resolvedActivationRoot = resolveInstallActivationRoot(resolvedOpenclawHome, activationRoot);
1858
+ const resolvedWorkspaceId = resolveInstallWorkspaceId(resolvedOpenclawHome, workspaceId);
1859
+ const resolvedEmbedderProvisionSkip = resolveInstallEmbedderProvisionSkip(skipEmbedderProvision);
1860
+ return {
1861
+ command,
1862
+ openclawHome: resolvedOpenclawHome,
1863
+ openclawHomeSource: "explicit",
1864
+ activationRoot: resolvedActivationRoot.activationRoot,
1865
+ activationRootSource: resolvedActivationRoot.source,
1866
+ shared,
1867
+ workspaceId: resolvedWorkspaceId.workspaceId,
1868
+ workspaceIdSource: resolvedWorkspaceId.source,
1869
+ skipEmbedderProvision: resolvedEmbedderProvisionSkip.skipEmbedderProvision,
1870
+ skipEmbedderProvisionSource: resolvedEmbedderProvisionSkip.skipEmbedderProvisionSource,
1182
1871
  json,
1183
1872
  help
1184
1873
  };
@@ -1240,30 +1929,6 @@ export function parseOperatorCliArgs(argv) {
1240
1929
  help
1241
1930
  };
1242
1931
  }
1243
- if (command === "attach") {
1244
- if (help) {
1245
- return { command, activationRoot: "", packRoot: "", packLabel: "", workspaceId: "", brainAttachmentPolicy: null, json, help };
1246
- }
1247
- if (activationRoot === null || activationRoot.trim().length === 0) {
1248
- throw new Error("--activation-root is required for attach");
1249
- }
1250
- const resolvedActivationRoot = path.resolve(activationRoot);
1251
- const resolvedPackRoot = packRoot !== null
1252
- ? path.resolve(packRoot)
1253
- : path.resolve(resolvedActivationRoot, "packs", "initial");
1254
- const resolvedWorkspaceId = workspaceId ?? "workspace";
1255
- const resolvedPackLabel = packLabel ?? "cli-attach";
1256
- return {
1257
- command,
1258
- activationRoot: resolvedActivationRoot,
1259
- packRoot: resolvedPackRoot,
1260
- packLabel: resolvedPackLabel,
1261
- workspaceId: resolvedWorkspaceId,
1262
- brainAttachmentPolicy: brainAttachmentPolicy,
1263
- json,
1264
- help
1265
- };
1266
- }
1267
1932
  if (command === "scan") {
1268
1933
  if ((sessionPath === null && livePath === null) || (sessionPath !== null && livePath !== null)) {
1269
1934
  throw new Error("scan requires exactly one of --session or --live");
@@ -1347,6 +2012,7 @@ function resolveExtensionRuntimeGuardPath() {
1347
2012
  path.resolve(__dirname, "..", "..", "extension", "runtime-guard.ts"),
1348
2013
  ];
1349
2014
  const jsCandidates = [
2015
+ path.resolve(__dirname, "..", "dist", "extension", "runtime-guard.js"),
1350
2016
  path.resolve(__dirname, "extension", "runtime-guard.js"),
1351
2017
  path.resolve(__dirname, "..", "extension", "runtime-guard.js"),
1352
2018
  ];
@@ -1736,15 +2402,33 @@ function runHistoryCommand(parsed) {
1736
2402
  }
1737
2403
  return 0;
1738
2404
  }
1739
- function runInstallCommand(parsed) {
2405
+ function runProfileHookAttachCommand(parsed) {
1740
2406
  const steps = [];
1741
2407
  const commandLabel = parsed.command.toUpperCase();
1742
- steps.push(`Target OpenClaw profile home: ${parsed.openclawHome} (${formatInstallOpenClawHomeSource(parsed.openclawHomeSource)})`);
2408
+ const isInstall = parsed.command === "install";
2409
+ const targetInspection = inspectOpenClawHome(parsed.openclawHome);
2410
+ const extensionDir = path.join(parsed.openclawHome, "extensions", "openclawbrain");
2411
+ steps.push(`Target OpenClaw home: ${parsed.openclawHome} (${formatInstallOpenClawHomeSource(parsed.openclawHomeSource)})`);
2412
+ steps.push(isInstall
2413
+ ? "Lifecycle mode: install is the safe first-time default for wiring one profile to one activation root."
2414
+ : "Lifecycle mode: attach is the explicit reattach/manual profile-hook path; use install for first-time setup.");
2415
+ steps.push(`Detected layout: ${formatOpenClawTargetExplanation(targetInspection)}`);
2416
+ steps.push(`Target hook path: ${extensionDir}`);
1743
2417
  // 1. Validate --openclaw-home exists and has openclaw.json
1744
2418
  validateOpenClawHome(parsed.openclawHome);
1745
2419
  // 2. Inspect the activation root before writing profile hook artifacts.
1746
2420
  const activationPlan = inspectInstallActivationPlan(parsed);
1747
- // 3. Create activation root if needed
2421
+ // 3. Ensure the default embedder exists before bootstrap unless the operator explicitly opts out.
2422
+ const embedderProvision = activationPlan.action === "bootstrap"
2423
+ ? ensureInstallEmbedderReady(parsed)
2424
+ : null;
2425
+ if (embedderProvision === null) {
2426
+ steps.push("Skipped bootstrap-time embedder provisioning because attach/install is reusing healthy activation state.");
2427
+ }
2428
+ else {
2429
+ steps.push(embedderProvision.detail);
2430
+ }
2431
+ // 4. Create activation root if needed
1748
2432
  if (activationPlan.createActivationRoot) {
1749
2433
  mkdirSync(parsed.activationRoot, { recursive: true });
1750
2434
  steps.push(`Created activation root: ${parsed.activationRoot}`);
@@ -1753,7 +2437,17 @@ function runInstallCommand(parsed) {
1753
2437
  steps.push(`Activation root exists: ${parsed.activationRoot}`);
1754
2438
  }
1755
2439
  steps.push(activationPlan.inspectionStep);
1756
- // 4. Bootstrap only for safe empty first-state roots; otherwise keep the inspected healthy state.
2440
+ // 5. Persist install-written local provider defaults so watch/learning surfaces do not depend on gateway env wiring.
2441
+ const providerDefaults = isInstall || activationPlan.action === "bootstrap"
2442
+ ? writeInstallProviderDefaults(parsed)
2443
+ : null;
2444
+ if (providerDefaults === null) {
2445
+ steps.push("Skipped provider-default refresh because explicit attach is reusing existing activation data.");
2446
+ }
2447
+ else {
2448
+ steps.push(providerDefaults.detail);
2449
+ }
2450
+ // 6. Bootstrap only for safe empty first-state roots; otherwise keep the inspected healthy state.
1757
2451
  if (activationPlan.action === "bootstrap") {
1758
2452
  const packRoot = path.resolve(parsed.activationRoot, "packs", "initial");
1759
2453
  mkdirSync(packRoot, { recursive: true });
@@ -1763,13 +2457,13 @@ function runInstallCommand(parsed) {
1763
2457
  brainAttachmentPolicy,
1764
2458
  activationRoot: parsed.activationRoot,
1765
2459
  packRoot,
1766
- packLabel: "install-cli",
2460
+ packLabel: isInstall ? "install-cli" : "attach-cli",
1767
2461
  workspace: {
1768
2462
  workspaceId: parsed.workspaceId,
1769
- snapshotId: `${parsed.workspaceId}@install-${new Date().toISOString().slice(0, 10)}`,
2463
+ snapshotId: `${parsed.workspaceId}@${parsed.command}-${new Date().toISOString().slice(0, 10)}`,
1770
2464
  capturedAt: new Date().toISOString(),
1771
2465
  rootDir: parsed.openclawHome,
1772
- revision: "cli-install-v1"
2466
+ revision: isInstall ? "cli-install-v1" : "cli-attach-v1"
1773
2467
  },
1774
2468
  interactionEvents: [],
1775
2469
  feedbackEvents: []
@@ -1777,10 +2471,11 @@ function runInstallCommand(parsed) {
1777
2471
  steps.push(`Bootstrapped brain attach: state=${result.currentProfile.brain.state} awaitingFirstExport=${yesNo(result.currentProfile.brainStatus.awaitingFirstExport)}`);
1778
2472
  }
1779
2473
  else {
1780
- steps.push(`Kept inspected activation state: active pack ${activationPlan.activePackId}`);
2474
+ steps.push(isInstall
2475
+ ? `Kept inspected activation state: active pack ${activationPlan.activePackId}`
2476
+ : `Reused inspected activation state for explicit attach: active pack ${activationPlan.activePackId}`);
1781
2477
  }
1782
- // 5-8. Write extension files
1783
- const extensionDir = path.join(parsed.openclawHome, "extensions", "openclawbrain");
2478
+ // 7-10. Write extension files
1784
2479
  mkdirSync(extensionDir, { recursive: true });
1785
2480
  // 5. Write index.ts
1786
2481
  const indexTsPath = path.join(extensionDir, "index.ts");
@@ -1833,91 +2528,40 @@ function runInstallCommand(parsed) {
1833
2528
  const manifestPath = path.join(extensionDir, "openclaw.plugin.json");
1834
2529
  writeFileSync(manifestPath, buildExtensionPluginManifest(), "utf8");
1835
2530
  steps.push(`Wrote manifest: ${manifestPath}`);
1836
- // 9. Write BRAIN.md to workspace directories
1837
- const brainMdContent = [
1838
- "## OpenClawBrain",
1839
- `You have a learning brain attached at ${parsed.activationRoot}.`,
1840
- "- It learns automatically from your conversations",
1841
- '- Corrections matter — "no, actually X" teaches the brain X',
1842
- "- You don't manage it — background daemon handles learning",
1843
- `- Check: \`openclawbrain status --activation-root ${quoteShellArg(parsed.activationRoot)}\``,
1844
- `- Rollback: \`openclawbrain rollback --activation-root ${quoteShellArg(parsed.activationRoot)}\``,
1845
- '- See what brain knows: `openclawbrain context "your question"`',
1846
- ""
1847
- ].join("\n");
1848
- const agentsMdBrainRef = "\n5. Read `BRAIN.md` — your learning brain context\n";
1849
- try {
1850
- const entries = readdirSync(parsed.openclawHome, { withFileTypes: true });
1851
- const workspaceDirs = entries
1852
- .filter(e => e.isDirectory() && e.name.startsWith("workspace-"))
1853
- .map(e => path.join(parsed.openclawHome, e.name));
1854
- // If no workspace-* dirs found, check if openclawHome itself is a workspace
1855
- if (workspaceDirs.length === 0) {
1856
- workspaceDirs.push(parsed.openclawHome);
1857
- }
1858
- for (const wsDir of workspaceDirs) {
1859
- const brainMdPath = path.join(wsDir, "BRAIN.md");
1860
- writeFileSync(brainMdPath, brainMdContent, "utf8");
1861
- steps.push(`Wrote BRAIN.md: ${brainMdPath}`);
1862
- // If AGENTS.md exists, append brain reference to startup sequence
1863
- const agentsMdPath = path.join(wsDir, "AGENTS.md");
1864
- if (existsSync(agentsMdPath)) {
1865
- const agentsContent = readFileSync(agentsMdPath, "utf8");
1866
- if (!agentsContent.includes("BRAIN.md")) {
1867
- // Find the startup sequence section and append after the last numbered item
1868
- const startupMarker = "## Session Startup";
1869
- if (agentsContent.includes(startupMarker)) {
1870
- // Find the numbered list in the startup section and append after last item
1871
- const lines = agentsContent.split("\n");
1872
- let lastNumberedIdx = -1;
1873
- let inStartup = false;
1874
- for (let i = 0; i < lines.length; i++) {
1875
- const line = lines[i] ?? "";
1876
- if (line.includes(startupMarker)) {
1877
- inStartup = true;
1878
- continue;
1879
- }
1880
- if (inStartup && /^\d+\.\s/.test(line.trim())) {
1881
- lastNumberedIdx = i;
1882
- }
1883
- if (inStartup && line.startsWith("## ") && !line.includes(startupMarker)) {
1884
- break;
1885
- }
1886
- }
1887
- if (lastNumberedIdx >= 0) {
1888
- lines.splice(lastNumberedIdx + 1, 0, agentsMdBrainRef.trimEnd());
1889
- writeFileSync(agentsMdPath, lines.join("\n"), "utf8");
1890
- steps.push(`Updated AGENTS.md startup sequence: ${agentsMdPath}`);
1891
- }
1892
- else {
1893
- appendFileSync(agentsMdPath, agentsMdBrainRef, "utf8");
1894
- steps.push(`Appended BRAIN.md reference to AGENTS.md: ${agentsMdPath}`);
1895
- }
1896
- }
1897
- else {
1898
- appendFileSync(agentsMdPath, agentsMdBrainRef, "utf8");
1899
- steps.push(`Appended BRAIN.md reference to AGENTS.md: ${agentsMdPath}`);
1900
- }
1901
- }
1902
- else {
1903
- steps.push(`AGENTS.md already references BRAIN.md: ${agentsMdPath}`);
1904
- }
1905
- }
1906
- }
1907
- }
1908
- catch (err) {
1909
- const message = err instanceof Error ? err.message : String(err);
1910
- steps.push(`BRAIN.md generation failed (non-fatal): ${message}`);
1911
- }
1912
2531
  const restartGuidance = buildInstallReloadGuidance();
1913
2532
  const nextSteps = [
1914
2533
  restartGuidance,
1915
- `Check status: ${buildInstallStatusCommand(parsed.activationRoot)}`
2534
+ `Check status: ${buildInstallStatusCommand(parsed.activationRoot)}`,
2535
+ embedderProvision !== null && embedderProvision.state === "skipped"
2536
+ ? `Provision default embedder later: ${buildInstallEmbedderProvisionCommand(embedderProvision.baseUrl, embedderProvision.model)}`
2537
+ : null
2538
+ ].filter((step) => step !== null);
2539
+ const preflightSummary = [
2540
+ `Hook: installed at ${shortenPath(extensionDir)}`,
2541
+ activationPlan.action === "bootstrap"
2542
+ ? "Attachment: seed/current-profile attach created; restart plus status will prove later serve-path use"
2543
+ : `Attachment: existing active pack ${activationPlan.activePackId} kept in place; restart plus status will prove later serve-path use`,
2544
+ embedderProvision === null
2545
+ ? "Embedder: unchanged because no bootstrap was needed"
2546
+ : embedderProvision.state === "ensured"
2547
+ ? `Embedder: default Ollama model ${embedderProvision.model} was ensured before bootstrap`
2548
+ : `Embedder: default Ollama model ${embedderProvision.model} was intentionally skipped`,
2549
+ `Serve path: install alone does not prove serving; restart the profile and run ${buildInstallStatusCommand(parsed.activationRoot)}`
1916
2550
  ];
1917
2551
  const lifecycleSummary = [
1918
- `OpenClaw profile: ${shortenPath(parsed.openclawHome)} (${formatInstallOpenClawHomeSource(parsed.openclawHomeSource)})`,
2552
+ isInstall
2553
+ ? "Lifecycle mode: install (safe first-time/default profile hookup)"
2554
+ : "Lifecycle mode: attach (explicit reattach/manual profile hookup)",
2555
+ `OpenClaw target: ${shortenPath(parsed.openclawHome)} (${formatInstallOpenClawHomeSource(parsed.openclawHomeSource)})`,
2556
+ `Detected layout: ${formatOpenClawTargetExplanation(targetInspection)}`,
1919
2557
  `Activation root: ${shortenPath(parsed.activationRoot)} (${formatInstallActivationRootSource(parsed.activationRootSource)})`,
1920
2558
  `Workspace ID: ${parsed.workspaceId} (${formatInstallWorkspaceIdSource(parsed.workspaceIdSource)})`,
2559
+ embedderProvision === null
2560
+ ? "Embedder: unchanged because no bootstrap was needed"
2561
+ : embedderProvision.state === "ensured"
2562
+ ? `Embedder: ensured default Ollama model ${embedderProvision.model} before brain init`
2563
+ : `Embedder: skipped default Ollama model ${embedderProvision.model} via ${parsed.skipEmbedderProvisionSource === "flag" ? "--skip-embedder-provision" : OPENCLAWBRAIN_INSTALL_SKIP_EMBEDDER_PROVISION_ENV}`,
2564
+ ...(providerDefaults === null ? [] : [`${providerDefaults.lifecycleSummary} (${shortenPath(providerDefaults.path)})`]),
1921
2565
  `Profile hook: installed at ${shortenPath(extensionDir)}`,
1922
2566
  activationPlan.resolution === "new_root"
1923
2567
  ? `Activation data: initialized at ${shortenPath(parsed.activationRoot)}`
@@ -1928,18 +2572,35 @@ function runInstallCommand(parsed) {
1928
2572
  : `Activation data: reused healthy state at ${shortenPath(parsed.activationRoot)}`,
1929
2573
  activationPlan.action === "bootstrap"
1930
2574
  ? activationPlan.resolution === "new_root"
1931
- ? "Brain attach: bootstrapped a seed/current-profile attach"
2575
+ ? `${isInstall ? "Install" : "Attach"}: bootstrapped a seed/current-profile brain`
1932
2576
  : activationPlan.resolution === "missing_pointers"
1933
- ? "Brain attach: repaired missing activation pointers and bootstrapped a seed/current-profile attach"
1934
- : "Brain attach: repaired empty activation pointers and bootstrapped a seed/current-profile attach"
1935
- : `Brain attach: kept healthy active pack ${activationPlan.activePackId} in place`
2577
+ ? `${isInstall ? "Install" : "Attach"}: repaired missing activation pointers and bootstrapped a seed/current-profile brain`
2578
+ : `${isInstall ? "Install" : "Attach"}: repaired empty activation pointers and bootstrapped a seed/current-profile brain`
2579
+ : isInstall
2580
+ ? `Install: kept healthy active pack ${activationPlan.activePackId} in place`
2581
+ : `Attach: rewired the profile hook to healthy active pack ${activationPlan.activePackId}`
1936
2582
  ];
2583
+ const brainFeedback = buildInstallBrainFeedbackSummary({
2584
+ parsed,
2585
+ targetInspection,
2586
+ extensionDir,
2587
+ activationPlan,
2588
+ embedderProvision,
2589
+ providerDefaults
2590
+ });
1937
2591
  // 9. Print summary
1938
2592
  if (parsed.json) {
1939
2593
  console.log(JSON.stringify({
1940
2594
  command: parsed.command,
1941
2595
  openclawHome: parsed.openclawHome,
1942
2596
  openclawHomeSource: parsed.openclawHomeSource,
2597
+ openclawTarget: {
2598
+ layout: targetInspection.layout,
2599
+ detail: describeOpenClawHomeInspection(targetInspection),
2600
+ profileId: targetInspection.profileId,
2601
+ profileSource: targetInspection.profileSource,
2602
+ configuredProfileIds: targetInspection.configuredProfileIds
2603
+ },
1943
2604
  activationRoot: parsed.activationRoot,
1944
2605
  resolvedInputs: {
1945
2606
  activationRoot: {
@@ -1953,8 +2614,47 @@ function runInstallCommand(parsed) {
1953
2614
  },
1954
2615
  workspaceId: parsed.workspaceId,
1955
2616
  shared: parsed.shared,
2617
+ embedderProvision: embedderProvision === null
2618
+ ? null
2619
+ : {
2620
+ skipped: parsed.skipEmbedderProvision,
2621
+ source: parsed.skipEmbedderProvisionSource,
2622
+ model: embedderProvision.model,
2623
+ baseUrl: embedderProvision.baseUrl
2624
+ },
2625
+ providerDefaults: providerDefaults === null
2626
+ ? null
2627
+ : {
2628
+ path: providerDefaults.path,
2629
+ teacher: providerDefaults.defaults.teacher === undefined
2630
+ ? null
2631
+ : {
2632
+ provider: providerDefaults.defaults.teacher.provider ?? null,
2633
+ model: providerDefaults.defaults.teacher.model ?? null,
2634
+ detectedLocally: providerDefaults.defaults.teacher.detectedLocally ?? false
2635
+ },
2636
+ embedder: providerDefaults.defaults.embedder === undefined
2637
+ ? null
2638
+ : {
2639
+ provider: providerDefaults.defaults.embedder.provider ?? null,
2640
+ model: providerDefaults.defaults.embedder.model ?? null
2641
+ },
2642
+ teacherBaseUrl: providerDefaults.defaults.teacherBaseUrl ?? null,
2643
+ embedderBaseUrl: providerDefaults.defaults.embedderBaseUrl ?? null
2644
+ },
2645
+ brainFeedback: {
2646
+ hookPath: brainFeedback.hookPath,
2647
+ providerDefaultsPath: brainFeedback.providerDefaultsPath,
2648
+ embedder: brainFeedback.embedder,
2649
+ teacher: brainFeedback.teacher,
2650
+ startup: brainFeedback.startup,
2651
+ provedNow: brainFeedback.provedNow,
2652
+ notYetProved: brainFeedback.notYetProved,
2653
+ lines: brainFeedback.lines
2654
+ },
1956
2655
  extensionDir,
1957
2656
  lifecycleSummary,
2657
+ preflightSummary,
1958
2658
  restartGuidance,
1959
2659
  nextSteps,
1960
2660
  steps
@@ -1962,19 +2662,24 @@ function runInstallCommand(parsed) {
1962
2662
  }
1963
2663
  else {
1964
2664
  console.log(`${commandLabel} complete\n`);
1965
- for (const step of steps) {
1966
- console.log(` ✓ ${step}`);
1967
- }
1968
- console.log("");
1969
- console.log("Lifecycle:");
1970
- for (const line of lifecycleSummary) {
2665
+ console.log("Brain feedback:");
2666
+ for (const line of brainFeedback.lines) {
1971
2667
  console.log(` ${line}`);
1972
2668
  }
1973
2669
  console.log(`Next: ${restartGuidance}`);
1974
2670
  console.log(`Check: ${buildInstallStatusCommand(parsed.activationRoot)}`);
2671
+ if (embedderProvision !== null && embedderProvision.state === "skipped") {
2672
+ console.log(`Embedder: ${buildInstallEmbedderProvisionCommand(embedderProvision.baseUrl, embedderProvision.model)}`);
2673
+ }
1975
2674
  }
1976
2675
  return 0;
1977
2676
  }
2677
+ function runInstallCommand(parsed) {
2678
+ return runProfileHookAttachCommand(parsed);
2679
+ }
2680
+ function runAttachCommand(parsed) {
2681
+ return runProfileHookAttachCommand(parsed);
2682
+ }
1978
2683
  function validateOpenClawHome(openclawHome) {
1979
2684
  if (!existsSync(openclawHome)) {
1980
2685
  throw new Error(`--openclaw-home directory does not exist: ${openclawHome}`);
@@ -2024,25 +2729,43 @@ function buildRestartGuidance(restart) {
2024
2729
  function runDetachCommand(parsed) {
2025
2730
  const steps = [];
2026
2731
  validateOpenClawHome(parsed.openclawHome);
2732
+ const targetInspection = inspectOpenClawHome(parsed.openclawHome);
2733
+ steps.push(`Detected layout: ${formatOpenClawTargetExplanation(targetInspection)}`);
2027
2734
  const activationRoot = resolveCleanupActivationRoot(parsed.openclawHome, parsed.activationRoot);
2028
2735
  const extensionDir = removeProfileHookup(parsed.openclawHome, steps);
2736
+ const legacyResidue = removeLegacyProfileResidue(parsed.openclawHome);
2029
2737
  const activationData = summarizeKeptActivationData(activationRoot);
2030
2738
  const restartGuidance = buildRestartGuidance(parsed.restart);
2031
2739
  const nextSteps = [
2032
2740
  restartGuidance,
2033
2741
  activationRoot === null ? null : `Inspect preserved data: ${buildInstallStatusCommand(activationRoot)}`,
2034
- `Reattach later: ${buildInstallCommand(parsed.openclawHome)}`
2742
+ `Reattach later: ${buildAttachCommand(parsed.openclawHome, activationRoot)}`
2035
2743
  ].filter((step) => step !== null);
2744
+ if (legacyResidue.removedNotes.length > 0) {
2745
+ steps.push(`Removed legacy profile notes: ${legacyResidue.removedNotes.map((notePath) => shortenPath(notePath)).join(", ")}`);
2746
+ }
2747
+ if (legacyResidue.updatedAgents.length > 0) {
2748
+ steps.push(`Removed legacy AGENTS.md brain references: ${legacyResidue.updatedAgents.map((agentsPath) => shortenPath(agentsPath)).join(", ")}`);
2749
+ }
2036
2750
  steps.push(activationData.activationDataDetail);
2037
2751
  steps.push("Detach only removes the OpenClaw profile hook; it does not delete OpenClawBrain data.");
2038
2752
  if (parsed.json) {
2039
2753
  console.log(JSON.stringify({
2040
2754
  command: "detach",
2041
2755
  openclawHome: parsed.openclawHome,
2756
+ openclawTarget: {
2757
+ layout: targetInspection.layout,
2758
+ detail: describeOpenClawHomeInspection(targetInspection),
2759
+ profileId: targetInspection.profileId,
2760
+ profileSource: targetInspection.profileSource,
2761
+ configuredProfileIds: targetInspection.configuredProfileIds
2762
+ },
2042
2763
  extensionDir,
2043
2764
  activationRoot,
2044
2765
  dataAction: "kept",
2045
2766
  activationDataState: activationData.activationDataState,
2767
+ removedLegacyNotes: legacyResidue.removedNotes,
2768
+ updatedAgents: legacyResidue.updatedAgents,
2046
2769
  restartMode: parsed.restart,
2047
2770
  restartGuidance,
2048
2771
  nextSteps,
@@ -2055,7 +2778,8 @@ function runDetachCommand(parsed) {
2055
2778
  console.log(` ✓ ${step}`);
2056
2779
  }
2057
2780
  console.log("");
2058
- console.log(`Lifecycle: OpenClaw profile ${shortenPath(parsed.openclawHome)} is detached from the brain hook.`);
2781
+ console.log(`Lifecycle: OpenClaw home ${shortenPath(parsed.openclawHome)} is detached from the brain hook.`);
2782
+ console.log(`Target: ${formatOpenClawTargetExplanation(targetInspection)}`);
2059
2783
  if (activationRoot !== null) {
2060
2784
  console.log(`Brain data: ${shortenPath(activationRoot)} remains available for inspection or reattach.`);
2061
2785
  }
@@ -2066,15 +2790,18 @@ function runDetachCommand(parsed) {
2066
2790
  if (activationRoot !== null) {
2067
2791
  console.log(`Check: ${buildInstallStatusCommand(activationRoot)}`);
2068
2792
  }
2069
- console.log(`Reattach: ${buildInstallCommand(parsed.openclawHome)}`);
2793
+ console.log(`Reattach: ${buildAttachCommand(parsed.openclawHome, activationRoot)}`);
2070
2794
  }
2071
2795
  return 0;
2072
2796
  }
2073
2797
  function runUninstallCommand(parsed) {
2074
2798
  const steps = [];
2075
2799
  validateOpenClawHome(parsed.openclawHome);
2800
+ const targetInspection = inspectOpenClawHome(parsed.openclawHome);
2801
+ steps.push(`Detected layout: ${formatOpenClawTargetExplanation(targetInspection)}`);
2076
2802
  const activationRoot = resolveCleanupActivationRoot(parsed.openclawHome, parsed.activationRoot);
2077
2803
  const extensionDir = removeProfileHookup(parsed.openclawHome, steps);
2804
+ const legacyResidue = removeLegacyProfileResidue(parsed.openclawHome);
2078
2805
  let activationData;
2079
2806
  if (parsed.dataMode === "purge") {
2080
2807
  if (activationRoot === null) {
@@ -2103,8 +2830,16 @@ function runUninstallCommand(parsed) {
2103
2830
  const nextSteps = [
2104
2831
  restartGuidance,
2105
2832
  parsed.dataMode === "keep" && activationRoot !== null ? `Inspect preserved data: ${buildInstallStatusCommand(activationRoot)}` : null,
2106
- `Reinstall later: ${buildInstallCommand(parsed.openclawHome)}`
2833
+ parsed.dataMode === "keep"
2834
+ ? `Reattach later: ${buildAttachCommand(parsed.openclawHome, activationRoot)}`
2835
+ : `Reinstall later: ${buildInstallCommand(parsed.openclawHome)}`
2107
2836
  ].filter((step) => step !== null);
2837
+ if (legacyResidue.removedNotes.length > 0) {
2838
+ steps.push(`Removed legacy profile notes: ${legacyResidue.removedNotes.map((notePath) => shortenPath(notePath)).join(", ")}`);
2839
+ }
2840
+ if (legacyResidue.updatedAgents.length > 0) {
2841
+ steps.push(`Removed legacy AGENTS.md brain references: ${legacyResidue.updatedAgents.map((agentsPath) => shortenPath(agentsPath)).join(", ")}`);
2842
+ }
2108
2843
  steps.push(activationData.activationDataDetail);
2109
2844
  steps.push(parsed.dataMode === "purge"
2110
2845
  ? "Uninstall removed the OpenClaw profile hook and activation data."
@@ -2113,10 +2848,19 @@ function runUninstallCommand(parsed) {
2113
2848
  console.log(JSON.stringify({
2114
2849
  command: "uninstall",
2115
2850
  openclawHome: parsed.openclawHome,
2851
+ openclawTarget: {
2852
+ layout: targetInspection.layout,
2853
+ detail: describeOpenClawHomeInspection(targetInspection),
2854
+ profileId: targetInspection.profileId,
2855
+ profileSource: targetInspection.profileSource,
2856
+ configuredProfileIds: targetInspection.configuredProfileIds
2857
+ },
2116
2858
  extensionDir,
2117
2859
  activationRoot,
2118
2860
  dataAction: parsed.dataMode,
2119
2861
  activationDataState: activationData.activationDataState,
2862
+ removedLegacyNotes: legacyResidue.removedNotes,
2863
+ updatedAgents: legacyResidue.updatedAgents,
2120
2864
  restartMode: parsed.restart,
2121
2865
  restartGuidance,
2122
2866
  nextSteps,
@@ -2129,7 +2873,8 @@ function runUninstallCommand(parsed) {
2129
2873
  console.log(` ✓ ${step}`);
2130
2874
  }
2131
2875
  console.log("");
2132
- console.log(`Lifecycle: OpenClaw profile ${shortenPath(parsed.openclawHome)} no longer has the brain hook installed.`);
2876
+ console.log(`Lifecycle: OpenClaw home ${shortenPath(parsed.openclawHome)} no longer has the brain hook installed.`);
2877
+ console.log(`Target: ${formatOpenClawTargetExplanation(targetInspection)}`);
2133
2878
  console.log(`Data mode: ${parsed.dataMode === "purge" ? "purged" : "kept"}`);
2134
2879
  if (activationRoot !== null) {
2135
2880
  console.log(`Activation: ${parsed.dataMode === "purge" ? shortenPath(activationRoot) : `${shortenPath(activationRoot)} preserved`}`);
@@ -2138,7 +2883,12 @@ function runUninstallCommand(parsed) {
2138
2883
  if (parsed.dataMode === "keep" && activationRoot !== null) {
2139
2884
  console.log(`Check: ${buildInstallStatusCommand(activationRoot)}`);
2140
2885
  }
2141
- console.log(`Reinstall: ${buildInstallCommand(parsed.openclawHome)}`);
2886
+ if (parsed.dataMode === "keep") {
2887
+ console.log(`Reattach: ${buildAttachCommand(parsed.openclawHome, activationRoot)}`);
2888
+ }
2889
+ else {
2890
+ console.log(`Reinstall: ${buildInstallCommand(parsed.openclawHome)}`);
2891
+ }
2142
2892
  }
2143
2893
  return 0;
2144
2894
  }
@@ -2782,15 +3532,18 @@ function applyWatchMaterialization(activationRoot, snapshot, lastHandledMaterial
2782
3532
  };
2783
3533
  }
2784
3534
  }
2785
- function resolveWatchTeacherLabelerConfig(input) {
3535
+ function resolveWatchTeacherLabelerConfig(input, activationRoot) {
2786
3536
  if (input !== undefined) {
2787
3537
  return {
2788
3538
  teacherLabeler: input,
2789
3539
  warnings: []
2790
3540
  };
2791
3541
  }
2792
- const providerConfig = readOpenClawBrainProviderConfig(process.env);
2793
- const warnings = providerConfig.warnings.filter((warning) => /OPENCLAWBRAIN_TEACHER_/u.test(warning));
3542
+ const providerConfig = readOpenClawBrainProviderConfigFromSources({
3543
+ env: process.env,
3544
+ activationRoot
3545
+ });
3546
+ const warnings = providerConfig.warnings.filter((warning) => /OPENCLAWBRAIN_TEACHER_|provider defaults/u.test(warning));
2794
3547
  if (providerConfig.teacher.provider !== "ollama") {
2795
3548
  return {
2796
3549
  teacherLabeler: null,
@@ -2832,11 +3585,11 @@ export async function createWatchCommandRuntime(input) {
2832
3585
  log(`Watch starting — activation: ${shortenPath(activationRoot)}`);
2833
3586
  log(`Scan root: ${shortenPath(scanRoot)}`);
2834
3587
  log(`State: cursor=${shortenPath(sessionTailCursorPath)} snapshot=${shortenPath(teacherSnapshotPath)}`);
2835
- const resolvedTeacherLabeler = resolveWatchTeacherLabelerConfig(input.teacherLabeler);
3588
+ const resolvedTeacherLabeler = resolveWatchTeacherLabelerConfig(input.teacherLabeler, activationRoot);
2836
3589
  const teacherLabeler = resolvedTeacherLabeler.teacherLabeler;
2837
3590
  for (const warning of resolvedTeacherLabeler.warnings) {
2838
- startupWarnings.push(`teacher_env_warning:${warning}`);
2839
- log(`Teacher env warning: ${warning}`);
3591
+ startupWarnings.push(`teacher_config_warning:${warning}`);
3592
+ log(`Teacher config warning: ${warning}`);
2840
3593
  }
2841
3594
  if (teacherLabeler?.provider === "ollama") {
2842
3595
  log(`Teacher labeler: provider=ollama model=${teacherLabeler.model ?? "qwen3.5:9b"}`);
@@ -3330,31 +4083,7 @@ export function runOperatorCli(argv = process.argv.slice(2)) {
3330
4083
  return runUninstallCommand(parsed);
3331
4084
  }
3332
4085
  if (parsed.command === "attach") {
3333
- mkdirSync(parsed.activationRoot, { recursive: true });
3334
- mkdirSync(parsed.packRoot, { recursive: true });
3335
- const result = bootstrapRuntimeAttach({
3336
- profileSelector: "current_profile",
3337
- ...(parsed.brainAttachmentPolicy != null ? { brainAttachmentPolicy: parsed.brainAttachmentPolicy } : {}),
3338
- activationRoot: parsed.activationRoot,
3339
- packRoot: parsed.packRoot,
3340
- packLabel: parsed.packLabel,
3341
- workspace: {
3342
- workspaceId: parsed.workspaceId,
3343
- snapshotId: `${parsed.workspaceId}@bootstrap-${new Date().toISOString().slice(0, 10)}`,
3344
- capturedAt: new Date().toISOString(),
3345
- rootDir: process.cwd(),
3346
- revision: "cli-bootstrap-v1"
3347
- },
3348
- interactionEvents: [],
3349
- feedbackEvents: []
3350
- });
3351
- if (parsed.json) {
3352
- console.log(JSON.stringify(result, null, 2));
3353
- }
3354
- else {
3355
- console.log(formatBootstrapRuntimeAttachReport(result));
3356
- }
3357
- return 0;
4086
+ return runAttachCommand(parsed);
3358
4087
  }
3359
4088
  if (parsed.command === "scan") {
3360
4089
  if (parsed.sessionPath !== null) {
@@ -3392,6 +4121,7 @@ export function runOperatorCli(argv = process.argv.slice(2)) {
3392
4121
  // At this point only status/rollback commands remain
3393
4122
  const statusOrRollback = parsed;
3394
4123
  const activationRoot = requireActivationRoot(statusOrRollback.input, statusOrRollback.openclawHome, statusOrRollback.command);
4124
+ const targetInspection = statusOrRollback.openclawHome === null ? null : inspectOpenClawHome(statusOrRollback.openclawHome);
3395
4125
  if (statusOrRollback.command === "rollback") {
3396
4126
  const result = rollbackRuntimeAttach({
3397
4127
  activationRoot,
@@ -3417,11 +4147,18 @@ export function runOperatorCli(argv = process.argv.slice(2)) {
3417
4147
  }
3418
4148
  else {
3419
4149
  const report = buildOperatorSurfaceReport(operatorInput);
4150
+ const providerConfig = readOpenClawBrainProviderConfig(process.env);
3420
4151
  if (statusOrRollback.detailed) {
3421
- console.log(formatCurrentProfileStatusSummary(status, report));
4152
+ console.log(formatCurrentProfileStatusSummary(status, report, targetInspection, {
4153
+ openclawHome: statusOrRollback.openclawHome,
4154
+ providerConfig
4155
+ }));
3422
4156
  }
3423
4157
  else {
3424
- console.log(formatHumanFriendlyStatus(status, report));
4158
+ console.log(formatHumanFriendlyStatus(status, report, targetInspection, {
4159
+ openclawHome: statusOrRollback.openclawHome,
4160
+ providerConfig
4161
+ }));
3425
4162
  }
3426
4163
  }
3427
4164
  return 0;