@openclawbrain/openclaw 0.2.3 → 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.
Files changed (44) hide show
  1. package/README.md +2 -1
  2. package/dist/extension/index.d.ts +1 -0
  3. package/dist/extension/index.js +81 -0
  4. package/dist/extension/index.js.map +1 -0
  5. package/dist/extension/runtime-guard.d.ts +61 -0
  6. package/dist/extension/runtime-guard.js +230 -0
  7. package/dist/extension/runtime-guard.js.map +1 -0
  8. package/dist/src/cli.d.ts +19 -9
  9. package/dist/src/cli.js +1464 -380
  10. package/dist/src/cli.js.map +1 -1
  11. package/dist/src/daemon.d.ts +7 -4
  12. package/dist/src/daemon.js +275 -47
  13. package/dist/src/daemon.js.map +1 -1
  14. package/dist/src/index.d.ts +150 -2
  15. package/dist/src/index.js +769 -139
  16. package/dist/src/index.js.map +1 -1
  17. package/dist/src/learning-spine.d.ts +2 -1
  18. package/dist/src/learning-spine.js +8 -0
  19. package/dist/src/learning-spine.js.map +1 -1
  20. package/dist/src/ollama-client.d.ts +46 -0
  21. package/dist/src/ollama-client.js +231 -0
  22. package/dist/src/ollama-client.js.map +1 -0
  23. package/dist/src/openclaw-home-layout.d.ts +17 -0
  24. package/dist/src/openclaw-home-layout.js +182 -0
  25. package/dist/src/openclaw-home-layout.js.map +1 -0
  26. package/dist/src/provider-config.d.ts +64 -0
  27. package/dist/src/provider-config.js +306 -0
  28. package/dist/src/provider-config.js.map +1 -0
  29. package/dist/src/resolve-activation-root.d.ts +5 -5
  30. package/dist/src/resolve-activation-root.js +76 -34
  31. package/dist/src/resolve-activation-root.js.map +1 -1
  32. package/dist/src/session-store.js +16 -5
  33. package/dist/src/session-store.js.map +1 -1
  34. package/dist/src/session-tail.js +14 -2
  35. package/dist/src/session-tail.js.map +1 -1
  36. package/dist/src/shadow-extension-proof.d.ts +40 -0
  37. package/dist/src/shadow-extension-proof.js +214 -0
  38. package/dist/src/shadow-extension-proof.js.map +1 -0
  39. package/dist/src/teacher-labeler.d.ts +50 -0
  40. package/dist/src/teacher-labeler.js +424 -0
  41. package/dist/src/teacher-labeler.js.map +1 -0
  42. package/extension/index.ts +22 -1
  43. package/extension/runtime-guard.ts +17 -2
  44. package/package.json +8 -7
package/dist/src/cli.js CHANGED
@@ -1,19 +1,30 @@
1
1
  #!/usr/bin/env node
2
- import { execSync } from "node:child_process";
3
- import { existsSync, mkdirSync, readFileSync, readdirSync, readSync, openSync, closeSync, realpathSync, rmSync, statSync, writeFileSync, appendFileSync, symlinkSync } from "node:fs";
2
+ import { execFileSync, execSync } from "node:child_process";
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
- import { inspectActivationState, promoteCandidatePack, readLearningSpineLogEntries, stageCandidatePack } from "@openclawbrain/pack-format";
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, loadRuntimeEventExportBundle, rollbackRuntimeAttach, resolveAsyncTeacherLiveLoopSnapshotPath, 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";
17
+ import { appendLearningUpdateLogs } from "./learning-spine.js";
15
18
  import { buildPassiveLearningSessionExportFromOpenClawSessionStore } from "./local-session-passive-learning.js";
16
19
  import { discoverOpenClawSessionStores, loadOpenClawSessionIndex, readOpenClawSessionFile } from "./session-store.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
+ ];
17
28
  function quoteShellArg(value) {
18
29
  return `'${value.replace(/'/g, `"'"'`)}'`;
19
30
  }
@@ -24,37 +35,32 @@ function normalizeOptionalCliString(value) {
24
35
  const trimmed = value.trim();
25
36
  return trimmed.length > 0 ? trimmed : null;
26
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
+ }
27
45
  function getCliHomeDir() {
28
46
  return process.env.HOME ?? process.env.USERPROFILE ?? "~";
29
47
  }
30
- function discoverSetupCandidateOpenClawHomes(homeDir = getCliHomeDir()) {
31
- const resolvedHomeDir = path.resolve(homeDir);
32
- let entries;
33
- try {
34
- entries = readdirSync(resolvedHomeDir, { withFileTypes: true });
35
- }
36
- catch {
37
- return [];
38
- }
39
- return entries
40
- .filter((entry) => entry.isDirectory() && entry.name.startsWith(".openclaw-"))
41
- .map((entry) => path.join(resolvedHomeDir, entry.name))
42
- .filter((candidate) => existsSync(path.join(candidate, "openclaw.json")))
43
- .sort((left, right) => left.localeCompare(right));
48
+ function discoverInstallCandidateOpenClawHomes(homeDir = getCliHomeDir()) {
49
+ return discoverOpenClawHomes(homeDir).map((inspection) => inspection.openclawHome);
44
50
  }
45
- function formatSetupOpenClawHomeSource(source) {
51
+ function formatInstallOpenClawHomeSource(source) {
46
52
  switch (source) {
47
53
  case "explicit":
48
54
  return "--openclaw-home";
49
55
  case "env":
50
56
  return "OPENCLAW_HOME";
51
- case "discovered_single_profile":
52
- return "single discovered live profile";
57
+ case "discovered_single_home":
58
+ return "single discovered install target";
53
59
  default:
54
60
  return source;
55
61
  }
56
62
  }
57
- function resolveSetupOpenClawHome(explicitOpenclawHome) {
63
+ function resolveInstallOpenClawHome(explicitOpenclawHome) {
58
64
  const normalizedExplicitHome = normalizeOptionalCliString(explicitOpenclawHome);
59
65
  if (normalizedExplicitHome !== null) {
60
66
  return {
@@ -69,30 +75,30 @@ function resolveSetupOpenClawHome(explicitOpenclawHome) {
69
75
  openclawHomeSource: "env"
70
76
  };
71
77
  }
72
- const discoveredHomes = discoverSetupCandidateOpenClawHomes();
78
+ const discoveredHomes = discoverInstallCandidateOpenClawHomes();
73
79
  if (discoveredHomes.length === 1) {
74
80
  return {
75
81
  openclawHome: path.resolve(discoveredHomes[0]),
76
- openclawHomeSource: "discovered_single_profile"
82
+ openclawHomeSource: "discovered_single_home"
77
83
  };
78
84
  }
79
85
  if (discoveredHomes.length > 1) {
80
86
  const installPrefix = detectConsumerSafeOperatorCliPrefix();
81
- const targetChoices = discoveredHomes
82
- .map((candidate) => {
83
- const resolvedCandidate = path.resolve(candidate);
84
- 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)}`;
85
91
  })
86
92
  .join("\n");
87
93
  throw new Error([
88
- "Refusing ambiguous live OpenClaw targets for install/setup.",
94
+ "Refusing ambiguous OpenClaw install targets.",
89
95
  targetChoices,
90
- "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."
91
97
  ].join("\n"));
92
98
  }
93
- 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.");
94
100
  }
95
- function resolveSetupActivationRoot(openclawHome, explicitActivationRoot) {
101
+ function resolveInstallActivationRoot(openclawHome, explicitActivationRoot) {
96
102
  const normalizedExplicitActivationRoot = normalizeOptionalCliString(explicitActivationRoot);
97
103
  if (normalizedExplicitActivationRoot !== null) {
98
104
  return {
@@ -105,7 +111,7 @@ function resolveSetupActivationRoot(openclawHome, explicitActivationRoot) {
105
111
  source: "default_from_openclaw_home"
106
112
  };
107
113
  }
108
- function resolveSetupWorkspaceId(openclawHome, explicitWorkspaceId) {
114
+ function resolveInstallWorkspaceId(openclawHome, explicitWorkspaceId) {
109
115
  const normalizedExplicitWorkspaceId = normalizeOptionalCliString(explicitWorkspaceId);
110
116
  if (normalizedExplicitWorkspaceId !== null) {
111
117
  return {
@@ -113,18 +119,24 @@ function resolveSetupWorkspaceId(openclawHome, explicitWorkspaceId) {
113
119
  source: "explicit"
114
120
  };
115
121
  }
116
- try {
117
- const openclawConfigPath = path.join(openclawHome, "openclaw.json");
118
- const openclawConfig = JSON.parse(readFileSync(openclawConfigPath, "utf8"));
119
- if (typeof openclawConfig.profile === "string" && openclawConfig.profile.trim().length > 0) {
120
- return {
121
- workspaceId: openclawConfig.profile.trim(),
122
- source: "openclaw_json_profile"
123
- };
124
- }
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
+ };
125
134
  }
126
- catch {
127
- // Fall back to the profile-home name when setup 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
+ };
128
140
  }
129
141
  const dirName = path.basename(openclawHome);
130
142
  if (dirName === ".openclaw") {
@@ -145,18 +157,40 @@ function resolveSetupWorkspaceId(openclawHome, explicitWorkspaceId) {
145
157
  source: "fallback"
146
158
  };
147
159
  }
148
- function formatSetupActivationRootSource(source) {
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
+ }
178
+ function formatInstallActivationRootSource(source) {
149
179
  if (source === "explicit") {
150
180
  return "explicit --activation-root";
151
181
  }
152
182
  return "default beside --openclaw-home";
153
183
  }
154
- function formatSetupWorkspaceIdSource(source) {
184
+ function formatInstallWorkspaceIdSource(source) {
155
185
  switch (source) {
156
186
  case "explicit":
157
187
  return "explicit --workspace-id";
158
188
  case "openclaw_json_profile":
159
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";
160
194
  case "openclaw_home_dir":
161
195
  return "from OpenClaw home dir";
162
196
  default:
@@ -262,7 +296,7 @@ function buildDoctorDeletedMessage(args) {
262
296
  const jsonCommand = buildStatusReplacementCommand(replacementInput, true);
263
297
  const lines = [
264
298
  "`doctor` is no longer a separate operator surface.",
265
- 'Use `openclawbrain status` as the human answer to "How\'s the brain?" and `status --json` for the canonical current-profile object.',
299
+ 'Use `openclawbrain status --activation-root <path>` as the human answer to "How\'s the brain?" and `status --json` for the canonical current-profile object.',
266
300
  "Use `describeAttachStatus()` or the proof helpers only when you need deeper activation diagnostics."
267
301
  ];
268
302
  if (json && jsonCommand !== null) {
@@ -276,36 +310,42 @@ function buildDoctorDeletedMessage(args) {
276
310
  }
277
311
  return lines.join(" ");
278
312
  }
313
+ function buildSetupDeletedMessage() {
314
+ return [
315
+ "`setup` has been removed.",
316
+ "Use `openclawbrain install` instead.",
317
+ "The install command still accepts the explicit targeting flags that setup used: `--openclaw-home`, `--activation-root`, `--workspace-id`, and `--shared`."
318
+ ].join(" ");
319
+ }
279
320
  function operatorCliHelp() {
280
321
  return [
281
322
  "Usage:",
282
323
  " openclawbrain install [--openclaw-home <path>] [options]",
283
- " openclawbrain setup [--openclaw-home <path>] [options] # compatibility alias",
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]",
284
328
  " openclawbrain detach --openclaw-home <path> [options]",
285
329
  " openclawbrain uninstall --openclaw-home <path> [--keep-data|--purge-data] [options]",
286
- " openclawbrain attach --activation-root <path> [options]",
287
- " openclawbrain <status|rollback> --activation-root <path> [options]",
288
- " openclawbrain context \"message\" [--activation-root <path>]",
289
- " openclawbrain history [--activation-root <path>] [--limit N] [--json]",
330
+ " openclawbrain context \"message\" [--activation-root <path>|--openclaw-home <path>]",
331
+ " openclawbrain history [--activation-root <path>|--openclaw-home <path>] [--limit N] [--json]",
290
332
  " openclawbrain scan --session <trace.json> --root <path> [options]",
291
333
  " openclawbrain scan --live <event-export-path> --workspace <workspace.json> [options]",
292
- " openclawbrain learn [--activation-root <path>] [--json]",
293
- " openclawbrain watch [--activation-root <path>] [--scan-root <path>] [--interval <seconds>]",
294
- " openclawbrain daemon <start|stop|status|logs> [--activation-root <path>]",
295
- " openclawbrain-ops <status|rollback> --activation-root <path> [options] # compatibility alias",
334
+ " openclawbrain learn [--activation-root <path>|--openclaw-home <path>] [--json]",
335
+ " openclawbrain-ops <status|rollback> [--activation-root <path>|--openclaw-home <path>] [options] # compatibility alias",
296
336
  " openclawbrain-ops scan --session <trace.json> --root <path> [options] # compatibility alias",
297
337
  "",
298
338
  "Options:",
299
- " --openclaw-home <path> OpenClaw profile home dir for install/setup/detach/uninstall (e.g. ~/.openclaw-Tern). Auto-selects for install/setup when OPENCLAW_HOME is set or exactly one live profile home exists.",
300
- " --shared Set brain-attachment-policy to shared instead of dedicated (install/setup only).",
301
- " --activation-root <path> Activation root for attach/detach/uninstall; install/setup 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.",
302
343
  " --keep-data Preserve activation data on uninstall; detach always behaves this way.",
303
344
  " --purge-data Remove activation data on uninstall; requires the installed profile hook or --activation-root.",
304
345
  " --restart <never|safe|external> Restart guidance mode for detach/uninstall. 'safe' is conservative; 'never' leaves restart entirely to the operator.",
305
- " --pack-root <path> Initial pack root directory (attach only; defaults to <activation-root>/packs/initial).",
306
- " --workspace-id <id> Workspace identifier for attach/install/setup provenance; install/setup 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.",
307
347
  " --event-export <path> Event-export bundle root or normalized export JSON payload.",
308
- " --teacher-snapshot <path> Async teacher snapshot JSON from teacherLoop.snapshot()/flush(); keeps live-first, principal-priority, and passive-backfill learner truth explicit.",
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.",
309
349
  " --updated-at <iso> Observation time to use for freshness checks.",
310
350
  " --brain-attachment-policy <undeclared|dedicated|shared> Override attachment policy semantics for status inspection.",
311
351
  " --detailed Show verbose diagnostic output for status (default is human-friendly summary).",
@@ -313,7 +353,7 @@ function operatorCliHelp() {
313
353
  " --session <path> Sanitized recorded-session trace JSON to replay.",
314
354
  " --live <path> Runtime event-export bundle root or normalized export JSON to scan once.",
315
355
  " --root <path> Output root for scan --session replay artifacts.",
316
- " --workspace <path> Workspace metadata JSON for scan --live candidate provenance.",
356
+ " --workspace <path> Workspace metadata JSON for scan --live candidate materialization.",
317
357
  " --pack-label <label> Candidate-pack label for scan --live. Defaults to scanner-live-cli.",
318
358
  " --observed-at <iso> Observation time for scan --live freshness checks.",
319
359
  " --snapshot-out <path> Write the one-shot scan --live snapshot JSON.",
@@ -323,24 +363,31 @@ function operatorCliHelp() {
323
363
  " --json Emit machine-readable JSON instead of text.",
324
364
  " --help Show this help.",
325
365
  "",
326
- "Common flow:",
327
- " 0. install openclawbrain install — attach the brain with sane defaults; pass --openclaw-home for explicit targeting on many-profile hosts",
328
- " 0. detach openclawbrain detach --openclaw-home <path> — remove the profile hook only; activation data stays in place",
329
- " 0. uninstall openclawbrain uninstall --openclaw-home <path> --keep-data|--purge-data remove the profile hook and choose the data outcome explicitly",
330
- " 0. context openclawbrain context \"hello\" preview the brain context that would be injected for a message",
331
- " 0. attach openclawbrain attach --activation-root <path>",
332
- " 1. status answer \"How's the brain?\" for the current profile on that activation root",
333
- " 2. status --json read the canonical current_profile_brain_status.v1 object for that same boundary",
334
- " 3. rollback --dry-run preview active <- previous, active -> candidate",
335
- " 4. rollback apply the rollback when the preview says ready",
336
- " 5. scan --session replay one sanitized session trace across no_brain, seed_pack, and learned_replay",
337
- " 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",
338
382
  " status --teacher-snapshot keeps the current live-first / principal-priority / passive-backfill learner order visible when that snapshot exists",
339
- " watch/daemon persist that snapshot at <activation-root>/async-teacher-live-loop.snapshot.json; --teacher-snapshot overrides the default path",
383
+ " watch/daemon persist their operator snapshot at <activation-root>/watch/teacher-snapshot.json; --teacher-snapshot overrides the default path",
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",
340
385
  "",
341
386
  "Exit codes:",
387
+ " install: 0 on successful profile hookup/bootstrap, 1 on input/read failure.",
342
388
  " status: 0 on successful inspection, 1 on input/read failure.",
343
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.",
344
391
  " detach: 0 on successful unhook, 1 on input/read failure.",
345
392
  " uninstall: 0 on successful unhook/cleanup, 1 on input/read failure.",
346
393
  " scan: 0 on successful replay/scan, 1 on input/read failure."
@@ -382,7 +429,8 @@ function formatLearningBuckets(report) {
382
429
  return `pi:${buckets.principal_immediate},pb:${buckets.principal_backfill},live:${buckets.live},backfill:${buckets.backfill}`;
383
430
  }
384
431
  function formatLearningWarnings(report) {
385
- 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("|");
386
434
  }
387
435
  function formatLabelFlowSummary(labelFlow) {
388
436
  return `source=${labelFlow.source} human=${labelFlow.humanLabelCount ?? "none"} self=${labelFlow.selfLabelCount ?? "none"} implicitPositive=${labelFlow.implicitPositiveCount ?? "none"} teacherArtifacts=${labelFlow.asyncTeacherArtifactCount ?? "none"}`;
@@ -390,14 +438,322 @@ function formatLabelFlowSummary(labelFlow) {
390
438
  function formatLearningPathSummary(learningPath) {
391
439
  return `source=${learningPath.source} pg=${learningPath.policyGradientVersion} method=${learningPath.policyGradientMethod ?? "none"} target=${learningPath.targetConstruction ?? "none"} connect=${learningPath.connectOpsFired ?? "none"} trajectories=${learningPath.reconstructedTrajectoryCount ?? "none"}`;
392
440
  }
393
- 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) {
394
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"))}`;
395
747
  return [
396
748
  `STATUS ${status.brainStatus.status}`,
749
+ ...buildCompactStatusHeader(status, report, options),
397
750
  `answer ${status.brain.summary}`,
751
+ targetLine,
752
+ ...(targetInspection === null ? [] : [`preflight ${formatOpenClawTargetExplanation(targetInspection)}`]),
398
753
  `host runtime=${status.host.runtimeOwner} activation=${status.host.activationRoot}`,
399
754
  `profile selector=${status.profile.selector}${profileIdSuffix} attachment=${status.attachment.state} policy=${status.attachment.policyMode}`,
400
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)}`,
756
+ `activation state=${status.brainStatus.activationState} detail=${status.brain.detail}`,
401
757
  `brain pack=${status.brain.activePackId ?? "none"} state=${status.brain.state} init=${status.brain.initMode ?? "unknown"} routeFreshness=${status.brain.routeFreshness} lastPromotion=${status.brain.lastPromotionAt ?? "none"} router=${status.brain.routerIdentity ?? "none"}`,
402
758
  `serve state=${status.brainStatus.serveState} failOpen=${yesNo(status.brainStatus.failOpen)} hardFail=${yesNo(report.servePath.hardRequirementViolated)} usedRouteFn=${yesNo(status.brainStatus.usedLearnedRouteFn)} awaitingFirstExport=${yesNo(status.brainStatus.awaitingFirstExport)} detail=${status.brainStatus.detail}`,
403
759
  `route router=${report.servePath.routerIdentity ?? status.brain.routerIdentity ?? "none"} supervision=${report.servePath.refreshStatus ?? status.brain.routeFreshness} freshness=${report.servePath.freshnessChecksum ?? "none"}`,
@@ -409,7 +765,8 @@ function formatCurrentProfileStatusSummary(status, report) {
409
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}`,
410
766
  `path ${formatLearningPathSummary(report.learningPath)}`,
411
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}`,
412
- `teacher snapshot=${report.teacherLoop.sourcePath ?? "none"} started=${report.teacherLoop.startedAt ?? "none"} heartbeat=${report.teacherLoop.lastHeartbeatAt ?? "none"} scan=${report.teacherLoop.lastScanAt ?? report.teacherLoop.lastProcessedAt ?? "none"} queue=${report.teacherLoop.queueDepth ?? "none"}/${report.teacherLoop.queueCapacity ?? "none"} running=${yesNo(report.teacherLoop.running)} lastJob=${report.teacherLoop.lastAppliedMaterializationJobId ?? "none"} lastPack=${report.teacherLoop.lastMaterializedPackId ?? "none"}`,
768
+ `teacher ${formatTeacherLoopSummary(report)}`,
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"}`,
413
770
  `rollback ready=${yesNo(report.rollback.allowed)} state=${report.rollback.state} previous=${report.rollback.previousPackId ?? "none"}`,
414
771
  `proof lastExport=${status.brain.lastExportAt ?? "none"} lastLearningUpdate=${status.brain.lastLearningUpdateAt ?? "none"} lastPromotion=${status.brain.lastPromotionAt ?? "none"}`,
415
772
  `logs root=${status.brain.logRoot ?? "none"}`,
@@ -426,15 +783,322 @@ function shortenPath(fullPath) {
426
783
  }
427
784
  return fullPath;
428
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
+ }
429
795
  function buildInstallStatusCommand(activationRoot) {
430
796
  return `openclawbrain status --activation-root ${quoteShellArg(activationRoot)}`;
431
797
  }
432
798
  function buildInstallCommand(openclawHome) {
433
799
  return `openclawbrain install --openclaw-home ${quoteShellArg(openclawHome)}`;
434
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
+ }
435
1026
  function buildInstallReloadGuidance() {
436
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.";
437
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
+ }
438
1102
  function buildCleanupRestartGuidance(restart) {
439
1103
  if (restart === "never") {
440
1104
  return "No restart requested. If this OpenClaw profile is currently running, it may keep the previous hook state until the next restart.";
@@ -445,64 +1109,56 @@ function buildCleanupRestartGuidance(restart) {
445
1109
  return "If this OpenClaw profile is currently running, restart it before expecting the new hook state to take effect. If it is stopped, the next launch will pick it up.";
446
1110
  }
447
1111
  function buildStatusNextStep(status, report) {
1112
+ const activationRootArg = quoteShellArg(status.host.activationRoot);
1113
+ if (status.brainStatus.activationState === "broken_install") {
1114
+ return "Repair or replace the activation root before trusting serve-path status again.";
1115
+ }
1116
+ if (status.brainStatus.activationState === "stale_incomplete") {
1117
+ return "Clean up or repair the retained activation state before reattaching or promoting packs.";
1118
+ }
448
1119
  if (status.brainStatus.status === "fail") {
449
- return "Run `openclawbrain status --detailed` before changing lifecycle state so the serve-path failure is explicit.";
1120
+ return `Run \`openclawbrain status --activation-root ${activationRootArg} --detailed\` before changing lifecycle state so the serve-path failure is explicit.`;
450
1121
  }
451
1122
  if (status.brainStatus.awaitingFirstExport) {
452
- return "Let the attached OpenClaw profile emit a real export, then rerun `openclawbrain status`.";
1123
+ return `Let the attached OpenClaw profile emit a real export, then rerun \`openclawbrain status --activation-root ${activationRootArg}\`.`;
453
1124
  }
454
1125
  if (report.learning.warningStates.includes("principal_live_backlog") ||
455
1126
  report.learning.warningStates.includes("active_pack_behind_latest_principal")) {
456
1127
  return "A newer principal correction is still pending promotion; keep the current pack conservative until learner promotion lands.";
457
1128
  }
458
1129
  if (report.rollback.allowed) {
459
- return "Use `openclawbrain rollback --dry-run` before restoring the previous pack.";
460
- }
461
- return "Use `openclawbrain status --detailed` when you need the full lifecycle, serve-path, and backlog proof.";
462
- }
463
- function formatHumanFriendlyStatus(status, report) {
464
- // Brain status line
465
- const brainActive = status.brainStatus.status === "ok" || status.brainStatus.serveState === "serving_active_pack";
466
- const brainIcon = brainActive ? "Active ✓" : status.brainStatus.status === "fail" ? "Inactive ✗" : `${status.brainStatus.status}`;
467
- // Pack line
468
- const packId = status.brain.activePackId ?? "none";
469
- const packShort = packId.length > 9 ? packId.slice(0, 9) : packId;
470
- const state = status.brain.state ?? "unknown";
471
- // Activation root
472
- const activationPath = shortenPath(status.host.activationRoot);
473
- // Policy
474
- const policy = status.attachment.policyMode ?? report.manyProfile.declaredAttachmentPolicy ?? "undeclared";
475
- const principalFrontier = formatPrincipalCheckpointFrontier(report);
476
- const pendingLive = String(report.learning.pendingLive ?? "none");
477
- const pendingBackfill = String(report.learning.pendingBackfill ?? "none");
478
- const nextLane = report.learning.nextPriorityLane ?? "none";
479
- const nextBucket = report.learning.nextPriorityBucket ?? "none";
1130
+ return `Use \`openclawbrain rollback --activation-root ${activationRootArg} --dry-run\` before restoring the previous pack.`;
1131
+ }
1132
+ return `Use \`openclawbrain status --activation-root ${activationRootArg} --detailed\` when you need the full lifecycle, serve-path, and backlog proof.`;
1133
+ }
1134
+ function formatHumanFriendlyStatus(status, report, targetInspection, options) {
480
1135
  const lines = [
481
- `Brain: ${brainIcon}`,
482
- `Pack: ${packShort} (${state})`,
483
- `Activation: ${activationPath}`,
484
- `Policy: ${policy}`,
485
- `Lifecycle: attachment=${status.attachment.state} serve=${status.brainStatus.serveState} awaitingFirstExport=${yesNo(status.brainStatus.awaitingFirstExport)}`,
486
- `Rollback: state=${report.rollback.state} ready=${yesNo(report.rollback.allowed)} previous=${report.rollback.previousPackId ?? "none"}`,
487
- `Backlog: principal=${principalFrontier} live=${pendingLive} backfill=${pendingBackfill} next=${nextLane}/${nextBucket}`,
488
- `Labels: ${formatLabelFlowSummary(report.labelFlow)}`,
489
- `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)}`
490
1143
  ];
491
- // Add learning/serve warnings if relevant
492
- if (report.learning.warningStates.length > 0) {
493
- lines.push(`Warnings: ${report.learning.warningStates.join(", ")}`);
1144
+ return lines.join("\n");
1145
+ }
1146
+ function requireActivationRoot(input, openclawHome, command) {
1147
+ const explicitActivationRoot = input.activationRoot.trim().length > 0 ? input.activationRoot : null;
1148
+ if (explicitActivationRoot !== null) {
1149
+ return path.resolve(explicitActivationRoot);
494
1150
  }
495
- if (status.brainStatus.awaitingFirstExport) {
496
- lines.push(`Note: Awaiting first event export`);
1151
+ if (openclawHome !== null) {
1152
+ return resolveActivationRoot({
1153
+ openclawHome
1154
+ });
497
1155
  }
498
- lines.push(`Next: ${buildStatusNextStep(status, report)}`);
499
- return lines.join("\n");
1156
+ throw new Error(`${command} requires --activation-root <path> or --openclaw-home <path>; unpinned host auto-resolution is no longer supported for ${command}.`);
500
1157
  }
501
- function requireActivationRoot(input, _command) {
502
- // Use the shared auto-detect chain for ALL commands:
503
- // explicit flag → ~/.openclawbrain/activation → extension scan → clear error
1158
+ function resolveCliActivationRoot(explicitActivationRoot, openclawHome) {
504
1159
  return resolveActivationRoot({
505
- explicit: input.activationRoot.trim().length > 0 ? input.activationRoot : null,
1160
+ explicit: explicitActivationRoot,
1161
+ openclawHome
506
1162
  });
507
1163
  }
508
1164
  function readJsonFile(filePath) {
@@ -530,10 +1186,18 @@ function formatScanSessionSummary(result) {
530
1186
  function formatScanLiveSummary(result, snapshotOutPath) {
531
1187
  const materializedPackId = result.snapshot.learner.lastMaterialization?.candidate.summary.packId ?? "none";
532
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
+ }
533
1197
  return [
534
1198
  "SCAN live ok",
535
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}`,
536
- `teacher artifacts=${result.snapshot.teacher.artifactCount} freshness=${result.snapshot.teacher.latestFreshness} humanLabels=${result.supervision.humanLabelCount} noop=${result.snapshot.diagnostics.lastNoOpReason}`,
1200
+ `teacher ${teacherSummary.join(" ")}`,
537
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"}`,
538
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"}`,
539
1203
  `learner packLabel=${result.packLabel} materialized=${materializedPackId} reason=${materializationReason}`,
@@ -553,12 +1217,12 @@ export function parseOperatorCliArgs(argv) {
553
1217
  let rootDir = null;
554
1218
  let workspacePath = null;
555
1219
  let packLabel = null;
556
- let packRoot = null;
557
1220
  let workspaceId = null;
558
1221
  let observedAt = null;
559
1222
  let snapshotOutPath = null;
560
1223
  let openclawHome = null;
561
1224
  let shared = false;
1225
+ let skipEmbedderProvision = false;
562
1226
  let keepData = false;
563
1227
  let purgeData = false;
564
1228
  let restart = "safe";
@@ -571,11 +1235,14 @@ export function parseOperatorCliArgs(argv) {
571
1235
  if (args[0] === "doctor") {
572
1236
  throw new Error(buildDoctorDeletedMessage(args.slice(1)));
573
1237
  }
1238
+ if (args[0] === "setup") {
1239
+ throw new Error(buildSetupDeletedMessage());
1240
+ }
574
1241
  if (args[0] === "daemon") {
575
1242
  args.shift();
576
1243
  return parseDaemonArgs(args);
577
1244
  }
578
- if (args[0] === "status" || args[0] === "rollback" || args[0] === "scan" || args[0] === "attach" || args[0] === "install" || args[0] === "setup" || args[0] === "detach" || args[0] === "uninstall" || args[0] === "context" || args[0] === "history" || args[0] === "learn" || args[0] === "watch" || args[0] === "export" || args[0] === "import" || args[0] === "reset") {
1245
+ if (args[0] === "status" || args[0] === "rollback" || args[0] === "scan" || args[0] === "attach" || args[0] === "install" || args[0] === "detach" || args[0] === "uninstall" || args[0] === "context" || args[0] === "history" || args[0] === "learn" || args[0] === "watch" || args[0] === "export" || args[0] === "import" || args[0] === "reset") {
579
1246
  command = args.shift();
580
1247
  }
581
1248
  if (command === "learn") {
@@ -598,6 +1265,15 @@ export function parseOperatorCliArgs(argv) {
598
1265
  index += 1;
599
1266
  continue;
600
1267
  }
1268
+ if (arg === "--openclaw-home") {
1269
+ const next = args[index + 1];
1270
+ if (next === undefined) {
1271
+ throw new Error("--openclaw-home requires a value");
1272
+ }
1273
+ openclawHome = next;
1274
+ index += 1;
1275
+ continue;
1276
+ }
601
1277
  if (arg.startsWith("--")) {
602
1278
  throw new Error(`unknown argument for learn: ${arg}`);
603
1279
  }
@@ -607,7 +1283,7 @@ export function parseOperatorCliArgs(argv) {
607
1283
  }
608
1284
  return {
609
1285
  command,
610
- activationRoot: resolveActivationRoot({ explicit: activationRoot }),
1286
+ activationRoot: resolveCliActivationRoot(activationRoot, openclawHome),
611
1287
  json,
612
1288
  help
613
1289
  };
@@ -663,9 +1339,12 @@ export function parseOperatorCliArgs(argv) {
663
1339
  if (help) {
664
1340
  return { command, activationRoot: "", scanRoot: null, interval: 30, json, help };
665
1341
  }
1342
+ if (activationRoot === null || activationRoot.trim().length === 0) {
1343
+ throw new Error("watch requires --activation-root <path>");
1344
+ }
666
1345
  return {
667
1346
  command,
668
- activationRoot: resolveActivationRoot({ explicit: activationRoot }),
1347
+ activationRoot: path.resolve(activationRoot),
669
1348
  scanRoot: watchScanRoot,
670
1349
  interval: watchInterval,
671
1350
  json,
@@ -693,6 +1372,15 @@ export function parseOperatorCliArgs(argv) {
693
1372
  index += 1;
694
1373
  continue;
695
1374
  }
1375
+ if (arg === "--openclaw-home") {
1376
+ const next = args[index + 1];
1377
+ if (next === undefined) {
1378
+ throw new Error("--openclaw-home requires a value");
1379
+ }
1380
+ openclawHome = next;
1381
+ index += 1;
1382
+ continue;
1383
+ }
696
1384
  if (arg.startsWith("--")) {
697
1385
  throw new Error(`unknown argument for context: ${arg}`);
698
1386
  }
@@ -707,7 +1395,7 @@ export function parseOperatorCliArgs(argv) {
707
1395
  return {
708
1396
  command,
709
1397
  message: messageParts.join(" "),
710
- activationRoot: resolveActivationRoot({ explicit: activationRoot }),
1398
+ activationRoot: resolveCliActivationRoot(activationRoot, openclawHome),
711
1399
  json,
712
1400
  help
713
1401
  };
@@ -733,6 +1421,15 @@ export function parseOperatorCliArgs(argv) {
733
1421
  index += 1;
734
1422
  continue;
735
1423
  }
1424
+ if (arg === "--openclaw-home") {
1425
+ const next = args[index + 1];
1426
+ if (next === undefined) {
1427
+ throw new Error("--openclaw-home requires a value");
1428
+ }
1429
+ openclawHome = next;
1430
+ index += 1;
1431
+ continue;
1432
+ }
736
1433
  if (arg === "--limit") {
737
1434
  const next = args[index + 1];
738
1435
  if (next === undefined) {
@@ -755,7 +1452,7 @@ export function parseOperatorCliArgs(argv) {
755
1452
  }
756
1453
  return {
757
1454
  command,
758
- activationRoot: resolveActivationRoot({ explicit: activationRoot }),
1455
+ activationRoot: resolveCliActivationRoot(activationRoot, openclawHome),
759
1456
  limit: historyLimit,
760
1457
  json,
761
1458
  help
@@ -781,6 +1478,14 @@ export function parseOperatorCliArgs(argv) {
781
1478
  index += 1;
782
1479
  continue;
783
1480
  }
1481
+ if (arg === "--openclaw-home") {
1482
+ const next = args[index + 1];
1483
+ if (next === undefined)
1484
+ throw new Error("--openclaw-home requires a value");
1485
+ openclawHome = next;
1486
+ index += 1;
1487
+ continue;
1488
+ }
784
1489
  if (arg === "-o" || arg === "--output") {
785
1490
  const next = args[index + 1];
786
1491
  if (next === undefined)
@@ -798,7 +1503,7 @@ export function parseOperatorCliArgs(argv) {
798
1503
  throw new Error("export requires -o <output.tar.gz>");
799
1504
  return {
800
1505
  command,
801
- activationRoot: resolveActivationRoot({ explicit: activationRoot }),
1506
+ activationRoot: resolveCliActivationRoot(activationRoot, openclawHome),
802
1507
  outputPath: path.resolve(outputPath),
803
1508
  json,
804
1509
  help,
@@ -829,6 +1534,14 @@ export function parseOperatorCliArgs(argv) {
829
1534
  index += 1;
830
1535
  continue;
831
1536
  }
1537
+ if (arg === "--openclaw-home") {
1538
+ const next = args[index + 1];
1539
+ if (next === undefined)
1540
+ throw new Error("--openclaw-home requires a value");
1541
+ openclawHome = next;
1542
+ index += 1;
1543
+ continue;
1544
+ }
832
1545
  if (arg.startsWith("--"))
833
1546
  throw new Error(`unknown argument for import: ${arg}`);
834
1547
  if (archivePath === null) {
@@ -845,7 +1558,7 @@ export function parseOperatorCliArgs(argv) {
845
1558
  return {
846
1559
  command,
847
1560
  archivePath: path.resolve(archivePath),
848
- activationRoot: resolveActivationRoot({ explicit: activationRoot }),
1561
+ activationRoot: resolveCliActivationRoot(activationRoot, openclawHome),
849
1562
  force,
850
1563
  json,
851
1564
  help,
@@ -875,13 +1588,21 @@ export function parseOperatorCliArgs(argv) {
875
1588
  index += 1;
876
1589
  continue;
877
1590
  }
1591
+ if (arg === "--openclaw-home") {
1592
+ const next = args[index + 1];
1593
+ if (next === undefined)
1594
+ throw new Error("--openclaw-home requires a value");
1595
+ openclawHome = next;
1596
+ index += 1;
1597
+ continue;
1598
+ }
878
1599
  throw new Error(`unknown argument for reset: ${arg}`);
879
1600
  }
880
1601
  if (help)
881
1602
  return { command, activationRoot: "", yes: false, json, help };
882
1603
  return {
883
1604
  command,
884
- activationRoot: resolveActivationRoot({ explicit: activationRoot }),
1605
+ activationRoot: resolveCliActivationRoot(activationRoot, openclawHome),
885
1606
  yes,
886
1607
  json,
887
1608
  help
@@ -905,6 +1626,10 @@ export function parseOperatorCliArgs(argv) {
905
1626
  shared = true;
906
1627
  continue;
907
1628
  }
1629
+ if (arg === "--skip-embedder-provision") {
1630
+ skipEmbedderProvision = true;
1631
+ continue;
1632
+ }
908
1633
  if (arg === "--keep-data") {
909
1634
  keepData = true;
910
1635
  continue;
@@ -1038,14 +1763,6 @@ export function parseOperatorCliArgs(argv) {
1038
1763
  index += 1;
1039
1764
  continue;
1040
1765
  }
1041
- if (arg === "--pack-root") {
1042
- if (next === undefined) {
1043
- throw new Error("--pack-root requires a value");
1044
- }
1045
- packRoot = next;
1046
- index += 1;
1047
- continue;
1048
- }
1049
1766
  if (arg === "--workspace-id") {
1050
1767
  if (next === undefined) {
1051
1768
  throw new Error("--workspace-id requires a value");
@@ -1059,13 +1776,28 @@ export function parseOperatorCliArgs(argv) {
1059
1776
  if (command !== "detach" && command !== "uninstall" && restartExplicitlySet) {
1060
1777
  throw new Error("--restart only applies to detach/uninstall");
1061
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
+ }
1062
1785
  if (command !== "uninstall" && keepData) {
1063
1786
  throw new Error("--keep-data only applies to uninstall; use detach to preserve activation data");
1064
1787
  }
1065
1788
  if (command !== "uninstall" && purgeData) {
1066
1789
  throw new Error("--purge-data only applies to uninstall");
1067
1790
  }
1068
- if (command === "install" || command === "setup") {
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
+ }
1800
+ if (command === "install") {
1069
1801
  if (help) {
1070
1802
  return {
1071
1803
  command,
@@ -1076,13 +1808,16 @@ export function parseOperatorCliArgs(argv) {
1076
1808
  shared: false,
1077
1809
  workspaceId: "",
1078
1810
  workspaceIdSource: "explicit",
1811
+ skipEmbedderProvision: false,
1812
+ skipEmbedderProvisionSource: null,
1079
1813
  json,
1080
1814
  help
1081
1815
  };
1082
1816
  }
1083
- const resolvedOpenclawHome = resolveSetupOpenClawHome(openclawHome);
1084
- const resolvedActivationRoot = resolveSetupActivationRoot(resolvedOpenclawHome.openclawHome, activationRoot);
1085
- const resolvedWorkspaceId = resolveSetupWorkspaceId(resolvedOpenclawHome.openclawHome, workspaceId);
1817
+ const resolvedOpenclawHome = resolveInstallOpenClawHome(openclawHome);
1818
+ const resolvedActivationRoot = resolveInstallActivationRoot(resolvedOpenclawHome.openclawHome, activationRoot);
1819
+ const resolvedWorkspaceId = resolveInstallWorkspaceId(resolvedOpenclawHome.openclawHome, workspaceId);
1820
+ const resolvedEmbedderProvisionSkip = resolveInstallEmbedderProvisionSkip(skipEmbedderProvision);
1086
1821
  return {
1087
1822
  command,
1088
1823
  openclawHome: resolvedOpenclawHome.openclawHome,
@@ -1092,6 +1827,47 @@ export function parseOperatorCliArgs(argv) {
1092
1827
  shared,
1093
1828
  workspaceId: resolvedWorkspaceId.workspaceId,
1094
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,
1095
1871
  json,
1096
1872
  help
1097
1873
  };
@@ -1153,30 +1929,6 @@ export function parseOperatorCliArgs(argv) {
1153
1929
  help
1154
1930
  };
1155
1931
  }
1156
- if (command === "attach") {
1157
- if (help) {
1158
- return { command, activationRoot: "", packRoot: "", packLabel: "", workspaceId: "", brainAttachmentPolicy: null, json, help };
1159
- }
1160
- if (activationRoot === null || activationRoot.trim().length === 0) {
1161
- throw new Error("--activation-root is required for attach");
1162
- }
1163
- const resolvedActivationRoot = path.resolve(activationRoot);
1164
- const resolvedPackRoot = packRoot !== null
1165
- ? path.resolve(packRoot)
1166
- : path.resolve(resolvedActivationRoot, "packs", "initial");
1167
- const resolvedWorkspaceId = workspaceId ?? "workspace";
1168
- const resolvedPackLabel = packLabel ?? "cli-attach";
1169
- return {
1170
- command,
1171
- activationRoot: resolvedActivationRoot,
1172
- packRoot: resolvedPackRoot,
1173
- packLabel: resolvedPackLabel,
1174
- workspaceId: resolvedWorkspaceId,
1175
- brainAttachmentPolicy: brainAttachmentPolicy,
1176
- json,
1177
- help
1178
- };
1179
- }
1180
1932
  if (command === "scan") {
1181
1933
  if ((sessionPath === null && livePath === null) || (sessionPath !== null && livePath !== null)) {
1182
1934
  throw new Error("scan requires exactly one of --session or --live");
@@ -1219,6 +1971,7 @@ export function parseOperatorCliArgs(argv) {
1219
1971
  updatedAt,
1220
1972
  brainAttachmentPolicy
1221
1973
  },
1974
+ openclawHome: normalizeOptionalCliString(openclawHome),
1222
1975
  json,
1223
1976
  help,
1224
1977
  dryRun,
@@ -1259,6 +2012,7 @@ function resolveExtensionRuntimeGuardPath() {
1259
2012
  path.resolve(__dirname, "..", "..", "extension", "runtime-guard.ts"),
1260
2013
  ];
1261
2014
  const jsCandidates = [
2015
+ path.resolve(__dirname, "..", "dist", "extension", "runtime-guard.js"),
1262
2016
  path.resolve(__dirname, "extension", "runtime-guard.js"),
1263
2017
  path.resolve(__dirname, "..", "extension", "runtime-guard.js"),
1264
2018
  ];
@@ -1282,6 +2036,36 @@ const LOCAL_WORKSPACE_EXTENSION_PACKAGES = [
1282
2036
  "provenance",
1283
2037
  "workspace-metadata"
1284
2038
  ];
2039
+ const OPENCLAWBRAIN_EXTENSION_TARBALL_DIR_ENV = "OPENCLAWBRAIN_EXTENSION_TARBALL_DIR";
2040
+ function resolveNpmCommand() {
2041
+ return process.platform === "win32" ? "npm.cmd" : "npm";
2042
+ }
2043
+ function resolveExtensionInstallReleaseTarballs() {
2044
+ const configuredDir = normalizeOptionalCliString(process.env[OPENCLAWBRAIN_EXTENSION_TARBALL_DIR_ENV]);
2045
+ if (configuredDir === null) {
2046
+ return null;
2047
+ }
2048
+ const artifactDir = path.resolve(configuredDir);
2049
+ let entries;
2050
+ try {
2051
+ entries = readdirSync(artifactDir, { withFileTypes: true });
2052
+ }
2053
+ catch (error) {
2054
+ const detail = error instanceof Error ? error.message : String(error);
2055
+ throw new Error(`${OPENCLAWBRAIN_EXTENSION_TARBALL_DIR_ENV} is unreadable: ${artifactDir} (${detail})`);
2056
+ }
2057
+ const tarballs = entries
2058
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".tgz"))
2059
+ .map((entry) => path.join(artifactDir, entry.name))
2060
+ .sort((left, right) => left.localeCompare(right));
2061
+ if (tarballs.length === 0) {
2062
+ throw new Error(`${OPENCLAWBRAIN_EXTENSION_TARBALL_DIR_ENV} has no .tgz release artifacts: ${artifactDir}`);
2063
+ }
2064
+ return {
2065
+ artifactDir,
2066
+ tarballs
2067
+ };
2068
+ }
1285
2069
  function resolveLocalWorkspaceRootForExtensionInstall() {
1286
2070
  const candidates = [
1287
2071
  path.resolve(__dirname, "..", "..", "..", ".."),
@@ -1458,6 +2242,85 @@ function buildHistoryEntry(record, slot, isActive) {
1458
2242
  current: isActive
1459
2243
  };
1460
2244
  }
2245
+ function formatInspectionFindings(findings) {
2246
+ return findings.join("; ");
2247
+ }
2248
+ function buildInstallRefusalError(parsed, detail) {
2249
+ const purgeCommand = `openclawbrain uninstall --openclaw-home ${quoteShellArg(parsed.openclawHome)} ` +
2250
+ `--activation-root ${quoteShellArg(parsed.activationRoot)} --purge-data`;
2251
+ return new Error(`Refusing to reuse activation root ${path.resolve(parsed.activationRoot)}: ${detail}. ` +
2252
+ "Install only repairs an empty first-state root; it will not overwrite populated or broken activation state. " +
2253
+ `Inspect: ${buildInstallStatusCommand(parsed.activationRoot)}. ` +
2254
+ `Reset: ${purgeCommand}.`);
2255
+ }
2256
+ function inspectInstallActivationPlan(parsed) {
2257
+ const resolvedActivationRoot = path.resolve(parsed.activationRoot);
2258
+ const activationPointersPath = path.join(resolvedActivationRoot, "activation-pointers.json");
2259
+ if (!existsSync(resolvedActivationRoot)) {
2260
+ return {
2261
+ createActivationRoot: true,
2262
+ action: "bootstrap",
2263
+ resolution: "new_root",
2264
+ inspectionStep: "Activation state inspection: activation root is missing; bootstrapping first state.",
2265
+ activePackId: null
2266
+ };
2267
+ }
2268
+ const activationRootStats = statSync(resolvedActivationRoot);
2269
+ if (!activationRootStats.isDirectory()) {
2270
+ throw buildInstallRefusalError(parsed, "activation root path exists but is not a directory");
2271
+ }
2272
+ if (!existsSync(activationPointersPath)) {
2273
+ return {
2274
+ createActivationRoot: false,
2275
+ action: "bootstrap",
2276
+ resolution: "missing_pointers",
2277
+ inspectionStep: "Activation state inspection: activation root exists but activation-pointers.json is missing; bootstrapping first state.",
2278
+ activePackId: null
2279
+ };
2280
+ }
2281
+ let inspection;
2282
+ try {
2283
+ inspection = inspectActivationState(resolvedActivationRoot, new Date().toISOString());
2284
+ }
2285
+ catch (error) {
2286
+ const detail = error instanceof Error ? error.message : String(error);
2287
+ throw buildInstallRefusalError(parsed, `activation pointers could not be inspected (${detail})`);
2288
+ }
2289
+ if (inspection.active === null && inspection.candidate === null && inspection.previous === null) {
2290
+ return {
2291
+ createActivationRoot: false,
2292
+ action: "bootstrap",
2293
+ resolution: "empty_pointers",
2294
+ inspectionStep: "Activation state inspection: activation pointers are present but all slots are empty; bootstrapping first state.",
2295
+ activePackId: null
2296
+ };
2297
+ }
2298
+ const unhealthySlots = [inspection.active, inspection.candidate, inspection.previous]
2299
+ .filter((slot) => slot !== null && !slot.activationReady)
2300
+ .map((slot) => `${slot.slot}: ${formatInspectionFindings(slot.findings)}`);
2301
+ if (unhealthySlots.length > 0) {
2302
+ throw buildInstallRefusalError(parsed, `activation state contains unhealthy slots (${unhealthySlots.join(" | ")})`);
2303
+ }
2304
+ if (inspection.active === null) {
2305
+ const populatedSlots = [inspection.candidate, inspection.previous]
2306
+ .filter((slot) => slot !== null)
2307
+ .map((slot) => slot.slot);
2308
+ throw buildInstallRefusalError(parsed, `activation state is populated without an active pack (${populatedSlots.join(", ")})`);
2309
+ }
2310
+ if (inspection.candidate !== null && !inspection.promotion.allowed) {
2311
+ throw buildInstallRefusalError(parsed, `candidate slot is stale or incoherent (${formatInspectionFindings(inspection.promotion.findings)})`);
2312
+ }
2313
+ if (inspection.previous !== null && !inspection.rollback.allowed) {
2314
+ throw buildInstallRefusalError(parsed, `previous slot is stale or incoherent (${formatInspectionFindings(inspection.rollback.findings)})`);
2315
+ }
2316
+ return {
2317
+ createActivationRoot: false,
2318
+ action: "keep",
2319
+ resolution: "healthy_existing",
2320
+ inspectionStep: `Activation state inspection: active pack ${inspection.active.packId} is healthy; keeping existing activation state.`,
2321
+ activePackId: inspection.active.packId
2322
+ };
2323
+ }
1461
2324
  function runHistoryCommand(parsed) {
1462
2325
  const activationRoot = parsed.activationRoot;
1463
2326
  const pointersPath = path.join(activationRoot, "activation-pointers.json");
@@ -1539,29 +2402,53 @@ function runHistoryCommand(parsed) {
1539
2402
  }
1540
2403
  return 0;
1541
2404
  }
1542
- function runSetupCommand(parsed) {
2405
+ function runProfileHookAttachCommand(parsed) {
1543
2406
  const steps = [];
1544
2407
  const commandLabel = parsed.command.toUpperCase();
1545
- const activationRootCreated = !existsSync(parsed.activationRoot);
1546
- let bootstrapped = false;
1547
- steps.push(`Target OpenClaw profile home: ${parsed.openclawHome} (${formatSetupOpenClawHomeSource(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}`);
1548
2417
  // 1. Validate --openclaw-home exists and has openclaw.json
1549
2418
  validateOpenClawHome(parsed.openclawHome);
1550
- // 2. Create activation root if needed
1551
- if (activationRootCreated) {
2419
+ // 2. Inspect the activation root before writing profile hook artifacts.
2420
+ const activationPlan = inspectInstallActivationPlan(parsed);
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
2432
+ if (activationPlan.createActivationRoot) {
1552
2433
  mkdirSync(parsed.activationRoot, { recursive: true });
1553
2434
  steps.push(`Created activation root: ${parsed.activationRoot}`);
1554
2435
  }
1555
2436
  else {
1556
2437
  steps.push(`Activation root exists: ${parsed.activationRoot}`);
1557
2438
  }
1558
- // 3. Run bootstrapRuntimeAttach if not already attached
1559
- const activationPointersPath = path.join(parsed.activationRoot, "activation-pointers.json");
1560
- if (existsSync(activationPointersPath)) {
1561
- steps.push("Brain already attached (activation-pointers.json exists), skipping bootstrap.");
2439
+ steps.push(activationPlan.inspectionStep);
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.");
1562
2446
  }
1563
2447
  else {
1564
- bootstrapped = true;
2448
+ steps.push(providerDefaults.detail);
2449
+ }
2450
+ // 6. Bootstrap only for safe empty first-state roots; otherwise keep the inspected healthy state.
2451
+ if (activationPlan.action === "bootstrap") {
1565
2452
  const packRoot = path.resolve(parsed.activationRoot, "packs", "initial");
1566
2453
  mkdirSync(packRoot, { recursive: true });
1567
2454
  const brainAttachmentPolicy = parsed.shared ? "shared" : "dedicated";
@@ -1570,27 +2457,31 @@ function runSetupCommand(parsed) {
1570
2457
  brainAttachmentPolicy,
1571
2458
  activationRoot: parsed.activationRoot,
1572
2459
  packRoot,
1573
- packLabel: "setup-cli",
2460
+ packLabel: isInstall ? "install-cli" : "attach-cli",
1574
2461
  workspace: {
1575
2462
  workspaceId: parsed.workspaceId,
1576
- snapshotId: `${parsed.workspaceId}@setup-${new Date().toISOString().slice(0, 10)}`,
2463
+ snapshotId: `${parsed.workspaceId}@${parsed.command}-${new Date().toISOString().slice(0, 10)}`,
1577
2464
  capturedAt: new Date().toISOString(),
1578
2465
  rootDir: parsed.openclawHome,
1579
- revision: "cli-setup-v1"
2466
+ revision: isInstall ? "cli-install-v1" : "cli-attach-v1"
1580
2467
  },
1581
2468
  interactionEvents: [],
1582
2469
  feedbackEvents: []
1583
2470
  });
1584
- steps.push(`Bootstrapped brain attach: ${result.status}`);
2471
+ steps.push(`Bootstrapped brain attach: state=${result.currentProfile.brain.state} awaitingFirstExport=${yesNo(result.currentProfile.brainStatus.awaitingFirstExport)}`);
1585
2472
  }
1586
- // 4-7. Write extension files
1587
- const extensionDir = path.join(parsed.openclawHome, "extensions", "openclawbrain");
2473
+ else {
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}`);
2477
+ }
2478
+ // 7-10. Write extension files
1588
2479
  mkdirSync(extensionDir, { recursive: true });
1589
- // 4. Write index.ts
2480
+ // 5. Write index.ts
1590
2481
  const indexTsPath = path.join(extensionDir, "index.ts");
1591
2482
  writeFileSync(indexTsPath, buildExtensionIndexTs(parsed.activationRoot), "utf8");
1592
2483
  steps.push(`Wrote extension: ${indexTsPath}`);
1593
- // 4b. Write runtime-guard files (imported by index.ts as ./runtime-guard.js)
2484
+ // 5b. Write runtime-guard files (imported by index.ts as ./runtime-guard.js)
1594
2485
  const runtimeGuardPaths = resolveExtensionRuntimeGuardPath();
1595
2486
  if (runtimeGuardPaths.ts !== null) {
1596
2487
  const runtimeGuardTsPath = path.join(extensionDir, "runtime-guard.ts");
@@ -1600,17 +2491,31 @@ function runSetupCommand(parsed) {
1600
2491
  const runtimeGuardJsPath = path.join(extensionDir, "runtime-guard.js");
1601
2492
  writeFileSync(runtimeGuardJsPath, readFileSync(runtimeGuardPaths.js, "utf8"), "utf8");
1602
2493
  steps.push(`Wrote extension runtime-guard: ${runtimeGuardJsPath}`);
1603
- // 5. Write package.json
2494
+ // 6. Write package.json
1604
2495
  const packageJsonPath = path.join(extensionDir, "package.json");
1605
2496
  writeFileSync(packageJsonPath, buildExtensionPackageJson(), "utf8");
1606
2497
  steps.push(`Wrote package.json: ${packageJsonPath}`);
1607
- // 6. npm install
2498
+ // 7. npm install
2499
+ const releaseTarballInstall = resolveExtensionInstallReleaseTarballs();
1608
2500
  try {
1609
- execSync("npm install --ignore-scripts", { cwd: extensionDir, stdio: "pipe" });
1610
- steps.push("Ran npm install --ignore-scripts");
2501
+ if (releaseTarballInstall !== null) {
2502
+ execFileSync(resolveNpmCommand(), ["install", "--ignore-scripts", "--no-save", ...releaseTarballInstall.tarballs], { cwd: extensionDir, stdio: "pipe" });
2503
+ steps.push(`Installed extension dependencies from release artifacts: ${releaseTarballInstall.tarballs.length} tarballs from ${releaseTarballInstall.artifactDir}`);
2504
+ }
2505
+ else {
2506
+ execSync("npm install --ignore-scripts", { cwd: extensionDir, stdio: "pipe" });
2507
+ steps.push("Ran npm install --ignore-scripts");
2508
+ const linkedPackages = installExtensionFromLocalWorkspaceBuild(extensionDir);
2509
+ if (linkedPackages !== null) {
2510
+ steps.push(`Linked coherent local workspace packages: ${linkedPackages.join(", ")}`);
2511
+ }
2512
+ }
1611
2513
  }
1612
2514
  catch (err) {
1613
2515
  const message = err instanceof Error ? err.message : String(err);
2516
+ if (releaseTarballInstall !== null) {
2517
+ throw new Error(`Extension dependency install from release artifacts failed: ${message}`);
2518
+ }
1614
2519
  const linkedPackages = installExtensionFromLocalWorkspaceBuild(extensionDir);
1615
2520
  if (linkedPackages !== null) {
1616
2521
  steps.push(`Linked coherent local workspace packages: ${linkedPackages.join(", ")}`);
@@ -1619,109 +2524,83 @@ function runSetupCommand(parsed) {
1619
2524
  steps.push(`npm install failed (non-fatal): ${message}`);
1620
2525
  }
1621
2526
  }
1622
- // 7. Write plugin manifest
2527
+ // 8. Write plugin manifest
1623
2528
  const manifestPath = path.join(extensionDir, "openclaw.plugin.json");
1624
2529
  writeFileSync(manifestPath, buildExtensionPluginManifest(), "utf8");
1625
2530
  steps.push(`Wrote manifest: ${manifestPath}`);
1626
- // 8. Write BRAIN.md to workspace directories
1627
- const brainMdContent = [
1628
- "## OpenClawBrain",
1629
- `You have a learning brain attached at ${parsed.activationRoot}.`,
1630
- "- It learns automatically from your conversations",
1631
- '- Corrections matter — "no, actually X" teaches the brain X',
1632
- "- You don't manage it — background daemon handles learning",
1633
- "- Check: `openclawbrain status`",
1634
- "- Rollback: `openclawbrain rollback`",
1635
- '- See what brain knows: `openclawbrain context "your question"`',
1636
- ""
1637
- ].join("\n");
1638
- const agentsMdBrainRef = "\n5. Read `BRAIN.md` — your learning brain context\n";
1639
- try {
1640
- const entries = readdirSync(parsed.openclawHome, { withFileTypes: true });
1641
- const workspaceDirs = entries
1642
- .filter(e => e.isDirectory() && e.name.startsWith("workspace-"))
1643
- .map(e => path.join(parsed.openclawHome, e.name));
1644
- // If no workspace-* dirs found, check if openclawHome itself is a workspace
1645
- if (workspaceDirs.length === 0) {
1646
- workspaceDirs.push(parsed.openclawHome);
1647
- }
1648
- for (const wsDir of workspaceDirs) {
1649
- const brainMdPath = path.join(wsDir, "BRAIN.md");
1650
- writeFileSync(brainMdPath, brainMdContent, "utf8");
1651
- steps.push(`Wrote BRAIN.md: ${brainMdPath}`);
1652
- // If AGENTS.md exists, append brain reference to startup sequence
1653
- const agentsMdPath = path.join(wsDir, "AGENTS.md");
1654
- if (existsSync(agentsMdPath)) {
1655
- const agentsContent = readFileSync(agentsMdPath, "utf8");
1656
- if (!agentsContent.includes("BRAIN.md")) {
1657
- // Find the startup sequence section and append after the last numbered item
1658
- const startupMarker = "## Session Startup";
1659
- if (agentsContent.includes(startupMarker)) {
1660
- // Find the numbered list in the startup section and append after last item
1661
- const lines = agentsContent.split("\n");
1662
- let lastNumberedIdx = -1;
1663
- let inStartup = false;
1664
- for (let i = 0; i < lines.length; i++) {
1665
- const line = lines[i] ?? "";
1666
- if (line.includes(startupMarker)) {
1667
- inStartup = true;
1668
- continue;
1669
- }
1670
- if (inStartup && /^\d+\.\s/.test(line.trim())) {
1671
- lastNumberedIdx = i;
1672
- }
1673
- if (inStartup && line.startsWith("## ") && !line.includes(startupMarker)) {
1674
- break;
1675
- }
1676
- }
1677
- if (lastNumberedIdx >= 0) {
1678
- lines.splice(lastNumberedIdx + 1, 0, agentsMdBrainRef.trimEnd());
1679
- writeFileSync(agentsMdPath, lines.join("\n"), "utf8");
1680
- steps.push(`Updated AGENTS.md startup sequence: ${agentsMdPath}`);
1681
- }
1682
- else {
1683
- appendFileSync(agentsMdPath, agentsMdBrainRef, "utf8");
1684
- steps.push(`Appended BRAIN.md reference to AGENTS.md: ${agentsMdPath}`);
1685
- }
1686
- }
1687
- else {
1688
- appendFileSync(agentsMdPath, agentsMdBrainRef, "utf8");
1689
- steps.push(`Appended BRAIN.md reference to AGENTS.md: ${agentsMdPath}`);
1690
- }
1691
- }
1692
- else {
1693
- steps.push(`AGENTS.md already references BRAIN.md: ${agentsMdPath}`);
1694
- }
1695
- }
1696
- }
1697
- }
1698
- catch (err) {
1699
- const message = err instanceof Error ? err.message : String(err);
1700
- steps.push(`BRAIN.md generation failed (non-fatal): ${message}`);
1701
- }
1702
2531
  const restartGuidance = buildInstallReloadGuidance();
1703
2532
  const nextSteps = [
1704
2533
  restartGuidance,
1705
- `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)}`
1706
2550
  ];
1707
2551
  const lifecycleSummary = [
1708
- `OpenClaw profile: ${shortenPath(parsed.openclawHome)} (${formatSetupOpenClawHomeSource(parsed.openclawHomeSource)})`,
1709
- `Activation root: ${shortenPath(parsed.activationRoot)} (${formatSetupActivationRootSource(parsed.activationRootSource)})`,
1710
- `Workspace ID: ${parsed.workspaceId} (${formatSetupWorkspaceIdSource(parsed.workspaceIdSource)})`,
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)}`,
2557
+ `Activation root: ${shortenPath(parsed.activationRoot)} (${formatInstallActivationRootSource(parsed.activationRootSource)})`,
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)})`]),
1711
2565
  `Profile hook: installed at ${shortenPath(extensionDir)}`,
1712
- activationRootCreated
2566
+ activationPlan.resolution === "new_root"
1713
2567
  ? `Activation data: initialized at ${shortenPath(parsed.activationRoot)}`
1714
- : `Activation data: reused at ${shortenPath(parsed.activationRoot)}`,
1715
- bootstrapped
1716
- ? "Brain attach: bootstrapped a seed/current-profile attach"
1717
- : "Brain attach: existing activation state kept in place"
2568
+ : activationPlan.resolution === "missing_pointers"
2569
+ ? `Activation data: repaired missing pointers at ${shortenPath(parsed.activationRoot)}`
2570
+ : activationPlan.resolution === "empty_pointers"
2571
+ ? `Activation data: repaired empty pointers at ${shortenPath(parsed.activationRoot)}`
2572
+ : `Activation data: reused healthy state at ${shortenPath(parsed.activationRoot)}`,
2573
+ activationPlan.action === "bootstrap"
2574
+ ? activationPlan.resolution === "new_root"
2575
+ ? `${isInstall ? "Install" : "Attach"}: bootstrapped a seed/current-profile brain`
2576
+ : activationPlan.resolution === "missing_pointers"
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}`
1718
2582
  ];
2583
+ const brainFeedback = buildInstallBrainFeedbackSummary({
2584
+ parsed,
2585
+ targetInspection,
2586
+ extensionDir,
2587
+ activationPlan,
2588
+ embedderProvision,
2589
+ providerDefaults
2590
+ });
1719
2591
  // 9. Print summary
1720
2592
  if (parsed.json) {
1721
2593
  console.log(JSON.stringify({
1722
2594
  command: parsed.command,
1723
2595
  openclawHome: parsed.openclawHome,
1724
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
+ },
1725
2604
  activationRoot: parsed.activationRoot,
1726
2605
  resolvedInputs: {
1727
2606
  activationRoot: {
@@ -1735,8 +2614,47 @@ function runSetupCommand(parsed) {
1735
2614
  },
1736
2615
  workspaceId: parsed.workspaceId,
1737
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
+ },
1738
2655
  extensionDir,
1739
2656
  lifecycleSummary,
2657
+ preflightSummary,
1740
2658
  restartGuidance,
1741
2659
  nextSteps,
1742
2660
  steps
@@ -1744,19 +2662,24 @@ function runSetupCommand(parsed) {
1744
2662
  }
1745
2663
  else {
1746
2664
  console.log(`${commandLabel} complete\n`);
1747
- for (const step of steps) {
1748
- console.log(` ✓ ${step}`);
1749
- }
1750
- console.log("");
1751
- console.log("Lifecycle:");
1752
- for (const line of lifecycleSummary) {
2665
+ console.log("Brain feedback:");
2666
+ for (const line of brainFeedback.lines) {
1753
2667
  console.log(` ${line}`);
1754
2668
  }
1755
2669
  console.log(`Next: ${restartGuidance}`);
1756
2670
  console.log(`Check: ${buildInstallStatusCommand(parsed.activationRoot)}`);
2671
+ if (embedderProvision !== null && embedderProvision.state === "skipped") {
2672
+ console.log(`Embedder: ${buildInstallEmbedderProvisionCommand(embedderProvision.baseUrl, embedderProvision.model)}`);
2673
+ }
1757
2674
  }
1758
2675
  return 0;
1759
2676
  }
2677
+ function runInstallCommand(parsed) {
2678
+ return runProfileHookAttachCommand(parsed);
2679
+ }
2680
+ function runAttachCommand(parsed) {
2681
+ return runProfileHookAttachCommand(parsed);
2682
+ }
1760
2683
  function validateOpenClawHome(openclawHome) {
1761
2684
  if (!existsSync(openclawHome)) {
1762
2685
  throw new Error(`--openclaw-home directory does not exist: ${openclawHome}`);
@@ -1806,25 +2729,43 @@ function buildRestartGuidance(restart) {
1806
2729
  function runDetachCommand(parsed) {
1807
2730
  const steps = [];
1808
2731
  validateOpenClawHome(parsed.openclawHome);
2732
+ const targetInspection = inspectOpenClawHome(parsed.openclawHome);
2733
+ steps.push(`Detected layout: ${formatOpenClawTargetExplanation(targetInspection)}`);
1809
2734
  const activationRoot = resolveCleanupActivationRoot(parsed.openclawHome, parsed.activationRoot);
1810
2735
  const extensionDir = removeProfileHookup(parsed.openclawHome, steps);
2736
+ const legacyResidue = removeLegacyProfileResidue(parsed.openclawHome);
1811
2737
  const activationData = summarizeKeptActivationData(activationRoot);
1812
2738
  const restartGuidance = buildRestartGuidance(parsed.restart);
1813
2739
  const nextSteps = [
1814
2740
  restartGuidance,
1815
2741
  activationRoot === null ? null : `Inspect preserved data: ${buildInstallStatusCommand(activationRoot)}`,
1816
- `Reattach later: ${buildInstallCommand(parsed.openclawHome)}`
2742
+ `Reattach later: ${buildAttachCommand(parsed.openclawHome, activationRoot)}`
1817
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
+ }
1818
2750
  steps.push(activationData.activationDataDetail);
1819
2751
  steps.push("Detach only removes the OpenClaw profile hook; it does not delete OpenClawBrain data.");
1820
2752
  if (parsed.json) {
1821
2753
  console.log(JSON.stringify({
1822
2754
  command: "detach",
1823
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
+ },
1824
2763
  extensionDir,
1825
2764
  activationRoot,
1826
2765
  dataAction: "kept",
1827
2766
  activationDataState: activationData.activationDataState,
2767
+ removedLegacyNotes: legacyResidue.removedNotes,
2768
+ updatedAgents: legacyResidue.updatedAgents,
1828
2769
  restartMode: parsed.restart,
1829
2770
  restartGuidance,
1830
2771
  nextSteps,
@@ -1837,7 +2778,8 @@ function runDetachCommand(parsed) {
1837
2778
  console.log(` ✓ ${step}`);
1838
2779
  }
1839
2780
  console.log("");
1840
- 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)}`);
1841
2783
  if (activationRoot !== null) {
1842
2784
  console.log(`Brain data: ${shortenPath(activationRoot)} remains available for inspection or reattach.`);
1843
2785
  }
@@ -1848,15 +2790,18 @@ function runDetachCommand(parsed) {
1848
2790
  if (activationRoot !== null) {
1849
2791
  console.log(`Check: ${buildInstallStatusCommand(activationRoot)}`);
1850
2792
  }
1851
- console.log(`Reattach: ${buildInstallCommand(parsed.openclawHome)}`);
2793
+ console.log(`Reattach: ${buildAttachCommand(parsed.openclawHome, activationRoot)}`);
1852
2794
  }
1853
2795
  return 0;
1854
2796
  }
1855
2797
  function runUninstallCommand(parsed) {
1856
2798
  const steps = [];
1857
2799
  validateOpenClawHome(parsed.openclawHome);
2800
+ const targetInspection = inspectOpenClawHome(parsed.openclawHome);
2801
+ steps.push(`Detected layout: ${formatOpenClawTargetExplanation(targetInspection)}`);
1858
2802
  const activationRoot = resolveCleanupActivationRoot(parsed.openclawHome, parsed.activationRoot);
1859
2803
  const extensionDir = removeProfileHookup(parsed.openclawHome, steps);
2804
+ const legacyResidue = removeLegacyProfileResidue(parsed.openclawHome);
1860
2805
  let activationData;
1861
2806
  if (parsed.dataMode === "purge") {
1862
2807
  if (activationRoot === null) {
@@ -1885,8 +2830,16 @@ function runUninstallCommand(parsed) {
1885
2830
  const nextSteps = [
1886
2831
  restartGuidance,
1887
2832
  parsed.dataMode === "keep" && activationRoot !== null ? `Inspect preserved data: ${buildInstallStatusCommand(activationRoot)}` : null,
1888
- `Reinstall later: ${buildInstallCommand(parsed.openclawHome)}`
2833
+ parsed.dataMode === "keep"
2834
+ ? `Reattach later: ${buildAttachCommand(parsed.openclawHome, activationRoot)}`
2835
+ : `Reinstall later: ${buildInstallCommand(parsed.openclawHome)}`
1889
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
+ }
1890
2843
  steps.push(activationData.activationDataDetail);
1891
2844
  steps.push(parsed.dataMode === "purge"
1892
2845
  ? "Uninstall removed the OpenClaw profile hook and activation data."
@@ -1895,10 +2848,19 @@ function runUninstallCommand(parsed) {
1895
2848
  console.log(JSON.stringify({
1896
2849
  command: "uninstall",
1897
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
+ },
1898
2858
  extensionDir,
1899
2859
  activationRoot,
1900
2860
  dataAction: parsed.dataMode,
1901
2861
  activationDataState: activationData.activationDataState,
2862
+ removedLegacyNotes: legacyResidue.removedNotes,
2863
+ updatedAgents: legacyResidue.updatedAgents,
1902
2864
  restartMode: parsed.restart,
1903
2865
  restartGuidance,
1904
2866
  nextSteps,
@@ -1911,7 +2873,8 @@ function runUninstallCommand(parsed) {
1911
2873
  console.log(` ✓ ${step}`);
1912
2874
  }
1913
2875
  console.log("");
1914
- 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)}`);
1915
2878
  console.log(`Data mode: ${parsed.dataMode === "purge" ? "purged" : "kept"}`);
1916
2879
  if (activationRoot !== null) {
1917
2880
  console.log(`Activation: ${parsed.dataMode === "purge" ? shortenPath(activationRoot) : `${shortenPath(activationRoot)} preserved`}`);
@@ -1920,7 +2883,12 @@ function runUninstallCommand(parsed) {
1920
2883
  if (parsed.dataMode === "keep" && activationRoot !== null) {
1921
2884
  console.log(`Check: ${buildInstallStatusCommand(activationRoot)}`);
1922
2885
  }
1923
- 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
+ }
1924
2892
  }
1925
2893
  return 0;
1926
2894
  }
@@ -2341,16 +3309,9 @@ function formatTimestamp() {
2341
3309
  function watchLog(message) {
2342
3310
  console.log(`${formatTimestamp()} ${message}`);
2343
3311
  }
2344
- function resolveOperatorTeacherSnapshotPath(activationRoot, explicitPath) {
2345
- if (explicitPath !== null && explicitPath !== undefined) {
2346
- return explicitPath;
2347
- }
2348
- const asyncSnapshotPath = resolveAsyncTeacherLiveLoopSnapshotPath(activationRoot);
2349
- return existsSync(asyncSnapshotPath) ? asyncSnapshotPath : null;
3312
+ function formatWatchError(error) {
3313
+ return error instanceof Error ? error.message : String(error);
2350
3314
  }
2351
- const WATCH_STATE_DIRNAME = "watch";
2352
- const WATCH_SESSION_TAIL_CURSOR_BASENAME = "session-tail-cursor.json";
2353
- const WATCH_TEACHER_SNAPSHOT_BASENAME = "teacher-snapshot.json";
2354
3315
  function sanitizeWatchPathSegment(value) {
2355
3316
  const sanitized = value
2356
3317
  .replace(/[^a-zA-Z0-9._-]+/g, "-")
@@ -2358,15 +3319,6 @@ function sanitizeWatchPathSegment(value) {
2358
3319
  .slice(0, 96);
2359
3320
  return sanitized.length > 0 ? sanitized : "session";
2360
3321
  }
2361
- function resolveWatchStateRoot(activationRoot) {
2362
- return path.resolve(activationRoot, WATCH_STATE_DIRNAME);
2363
- }
2364
- function resolveWatchSessionTailCursorPath(activationRoot) {
2365
- return path.join(resolveWatchStateRoot(activationRoot), WATCH_SESSION_TAIL_CURSOR_BASENAME);
2366
- }
2367
- function resolveWatchTeacherSnapshotPath(activationRoot) {
2368
- return path.join(resolveWatchStateRoot(activationRoot), WATCH_TEACHER_SNAPSHOT_BASENAME);
2369
- }
2370
3322
  function readOptionalJsonFile(filePath) {
2371
3323
  if (!existsSync(filePath)) {
2372
3324
  return null;
@@ -2400,28 +3352,8 @@ function persistWatchSessionTailCursor(cursorPath, cursor) {
2400
3352
  cursor
2401
3353
  });
2402
3354
  }
2403
- function loadWatchTeacherSnapshotState(snapshotPath) {
2404
- const parsed = readOptionalJsonFile(snapshotPath);
2405
- return {
2406
- lastHandledMaterializationPackId: parsed !== null && typeof parsed.lastHandledMaterializationPackId === "string"
2407
- ? parsed.lastHandledMaterializationPackId
2408
- : null
2409
- };
2410
- }
2411
- function persistWatchTeacherSnapshot(snapshotPath, input) {
2412
- writeJsonFile(snapshotPath, {
2413
- contract: "openclaw_watch_teacher_snapshot.v1",
2414
- runtimeOwner: "openclaw",
2415
- updatedAt: new Date().toISOString(),
2416
- scanRoot: input.scanRoot,
2417
- replayedBundleCount: input.replayedBundleCount,
2418
- replayedEventCount: input.replayedEventCount,
2419
- exportedBundleCount: input.exportedBundleCount,
2420
- exportedEventCount: input.exportedEventCount,
2421
- localSessionTailNoopReason: input.localSessionTailNoopReason,
2422
- lastHandledMaterializationPackId: input.lastHandledMaterializationPackId,
2423
- snapshot: input.snapshot
2424
- });
3355
+ function countWatchCursorBridgedEvents(cursor) {
3356
+ return cursor.reduce((sum, entry) => sum + entry.bridgedEventCount, 0);
2425
3357
  }
2426
3358
  function listWatchRuntimeEventExportBundleRoots(scanRoot) {
2427
3359
  if (!existsSync(scanRoot)) {
@@ -2528,7 +3460,8 @@ function applyWatchMaterialization(activationRoot, snapshot, lastHandledMaterial
2528
3460
  return {
2529
3461
  lastHandledMaterializationPackId,
2530
3462
  logLine: null,
2531
- materializedPackId: null
3463
+ materializedPackId: null,
3464
+ failure: null
2532
3465
  };
2533
3466
  }
2534
3467
  const packId = typeof materialization?.candidate?.summary?.packId === "string"
@@ -2538,14 +3471,28 @@ function applyWatchMaterialization(activationRoot, snapshot, lastHandledMaterial
2538
3471
  return {
2539
3472
  lastHandledMaterializationPackId,
2540
3473
  logLine: null,
2541
- materializedPackId: packId
3474
+ materializedPackId: packId,
3475
+ failure: null
2542
3476
  };
2543
3477
  }
2544
3478
  const shortPackId = packId.length > 16 ? packId.slice(0, 16) : packId;
2545
3479
  try {
2546
3480
  const candidateRootDir = path.resolve(activationRoot, "packs", packId);
2547
3481
  mkdirSync(candidateRootDir, { recursive: true });
2548
- materializeAlwaysOnLearningCandidatePack(candidateRootDir, materialization);
3482
+ let activeBeforePack = null;
3483
+ try {
3484
+ activeBeforePack = loadPackFromActivation(activationRoot, "active", { requireActivationReady: true });
3485
+ }
3486
+ catch {
3487
+ activeBeforePack = null;
3488
+ }
3489
+ const candidateDescriptor = materializeAlwaysOnLearningCandidatePack(candidateRootDir, materialization);
3490
+ appendLearningUpdateLogs({
3491
+ activationRoot,
3492
+ materialization,
3493
+ activeBeforePack,
3494
+ candidateDescriptor
3495
+ });
2549
3496
  const now = new Date().toISOString();
2550
3497
  stageCandidatePack(activationRoot, candidateRootDir, {
2551
3498
  updatedAt: now,
@@ -2560,13 +3507,15 @@ function applyWatchMaterialization(activationRoot, snapshot, lastHandledMaterial
2560
3507
  return {
2561
3508
  lastHandledMaterializationPackId: packId,
2562
3509
  materializedPackId: packId,
2563
- logLine: `Promoted ${shortPackId} → active`
3510
+ logLine: `Promoted ${shortPackId} → active`,
3511
+ failure: null
2564
3512
  };
2565
3513
  }
2566
3514
  return {
2567
3515
  lastHandledMaterializationPackId: packId,
2568
3516
  materializedPackId: packId,
2569
- logLine: `Staged ${shortPackId} (promotion blocked: ${inspection.promotion.findings.join(", ")})`
3517
+ logLine: `Staged ${shortPackId} (promotion blocked: ${inspection.promotion.findings.join(", ")})`,
3518
+ failure: null
2570
3519
  };
2571
3520
  }
2572
3521
  catch (error) {
@@ -2574,26 +3523,80 @@ function applyWatchMaterialization(activationRoot, snapshot, lastHandledMaterial
2574
3523
  return {
2575
3524
  lastHandledMaterializationPackId,
2576
3525
  materializedPackId: packId,
2577
- logLine: `Promotion failed for ${shortPackId}: ${message}`
3526
+ logLine: `Promotion failed for ${shortPackId}: ${message}`,
3527
+ failure: {
3528
+ mode: "materialization_failed",
3529
+ detail: message,
3530
+ at: new Date().toISOString()
3531
+ }
2578
3532
  };
2579
3533
  }
2580
3534
  }
3535
+ function resolveWatchTeacherLabelerConfig(input, activationRoot) {
3536
+ if (input !== undefined) {
3537
+ return {
3538
+ teacherLabeler: input,
3539
+ warnings: []
3540
+ };
3541
+ }
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));
3547
+ if (providerConfig.teacher.provider !== "ollama") {
3548
+ return {
3549
+ teacherLabeler: null,
3550
+ warnings
3551
+ };
3552
+ }
3553
+ return {
3554
+ teacherLabeler: {
3555
+ provider: "ollama",
3556
+ baseUrl: providerConfig.teacherBaseUrl,
3557
+ model: providerConfig.teacher.model,
3558
+ ...(providerConfig.teacher.timeoutMs === undefined ? {} : { timeoutMs: providerConfig.teacher.timeoutMs }),
3559
+ ...(providerConfig.teacher.maxPromptChars === undefined ? {} : { maxPromptChars: providerConfig.teacher.maxPromptChars }),
3560
+ ...(providerConfig.teacher.maxResponseChars === undefined ? {} : { maxResponseChars: providerConfig.teacher.maxResponseChars }),
3561
+ ...(providerConfig.teacher.maxOutputTokens === undefined ? {} : { maxOutputTokens: providerConfig.teacher.maxOutputTokens }),
3562
+ ...(providerConfig.teacher.maxArtifactsPerExport === undefined
3563
+ ? {}
3564
+ : { maxArtifactsPerExport: providerConfig.teacher.maxArtifactsPerExport }),
3565
+ ...(providerConfig.teacher.maxInteractionsPerExport === undefined
3566
+ ? {}
3567
+ : { maxInteractionsPerExport: providerConfig.teacher.maxInteractionsPerExport })
3568
+ },
3569
+ warnings
3570
+ };
3571
+ }
2581
3572
  export async function createWatchCommandRuntime(input) {
2582
3573
  const activationRoot = path.resolve(input.activationRoot);
3574
+ const bootstrapObservedAt = new Date().toISOString();
2583
3575
  const scanRoot = input.scanRoot !== undefined && input.scanRoot !== null
2584
3576
  ? path.resolve(input.scanRoot)
2585
3577
  : path.resolve(activationRoot, "event-exports");
2586
3578
  const sessionTailCursorPath = resolveWatchSessionTailCursorPath(activationRoot);
2587
3579
  const teacherSnapshotPath = resolveWatchTeacherSnapshotPath(activationRoot);
3580
+ const restoredTeacherState = loadWatchTeacherSnapshotState(teacherSnapshotPath);
2588
3581
  const log = input.log ?? watchLog;
3582
+ const startupWarnings = [];
2589
3583
  mkdirSync(scanRoot, { recursive: true });
2590
3584
  mkdirSync(resolveWatchStateRoot(activationRoot), { recursive: true });
2591
3585
  log(`Watch starting — activation: ${shortenPath(activationRoot)}`);
2592
3586
  log(`Scan root: ${shortenPath(scanRoot)}`);
2593
3587
  log(`State: cursor=${shortenPath(sessionTailCursorPath)} snapshot=${shortenPath(teacherSnapshotPath)}`);
3588
+ const resolvedTeacherLabeler = resolveWatchTeacherLabelerConfig(input.teacherLabeler, activationRoot);
3589
+ const teacherLabeler = resolvedTeacherLabeler.teacherLabeler;
3590
+ for (const warning of resolvedTeacherLabeler.warnings) {
3591
+ startupWarnings.push(`teacher_config_warning:${warning}`);
3592
+ log(`Teacher config warning: ${warning}`);
3593
+ }
3594
+ if (teacherLabeler?.provider === "ollama") {
3595
+ log(`Teacher labeler: provider=ollama model=${teacherLabeler.model ?? "qwen3.5:9b"}`);
3596
+ }
2594
3597
  const scanner = createRuntimeEventExportScanner({ scanRoot });
2595
3598
  let lastServeTimeFallbackReason = null;
2596
- const teacherLoop = createAsyncTeacherLiveLoop({
3599
+ const baseTeacherLoopInput = {
2597
3600
  packLabel: "watch-cli",
2598
3601
  workspace: {
2599
3602
  workspaceId: "watch-cli",
@@ -2603,6 +3606,7 @@ export async function createWatchCommandRuntime(input) {
2603
3606
  revision: "watch-cli-v2"
2604
3607
  },
2605
3608
  learnedRouting: true,
3609
+ ...(teacherLabeler !== null ? { teacherLabeler } : {}),
2606
3610
  resolveLearnedRoutingState: () => {
2607
3611
  const resolved = resolveServeTimeLearningRuntimeInput(activationRoot);
2608
3612
  if (resolved.fallbackReason !== null && resolved.fallbackReason !== lastServeTimeFallbackReason) {
@@ -2620,11 +3624,38 @@ export async function createWatchCommandRuntime(input) {
2620
3624
  persistBaseline(activationRoot, state);
2621
3625
  }
2622
3626
  catch (error) {
2623
- const message = error instanceof Error ? error.message : String(error);
2624
- log(`Baseline persist failed: ${message}`);
3627
+ log(`Baseline persist failed: ${formatWatchError(error)}`);
2625
3628
  }
2626
3629
  }
2627
- });
3630
+ };
3631
+ let teacherLoop;
3632
+ let lastHandledMaterializationPackId = restoredTeacherState.lastHandledMaterializationPackId;
3633
+ if (restoredTeacherState.error !== null) {
3634
+ const message = restoredTeacherState.error;
3635
+ startupWarnings.push(`teacher_snapshot_reset:${message}`);
3636
+ lastHandledMaterializationPackId = null;
3637
+ log(`Teacher snapshot reset: ${message}`);
3638
+ teacherLoop = createAsyncTeacherLiveLoop(baseTeacherLoopInput);
3639
+ }
3640
+ else {
3641
+ try {
3642
+ teacherLoop = createAsyncTeacherLiveLoop({
3643
+ ...baseTeacherLoopInput,
3644
+ ...(restoredTeacherState.snapshot !== null ? { resumeFromSnapshot: restoredTeacherState.snapshot } : {})
3645
+ });
3646
+ }
3647
+ catch (error) {
3648
+ const message = formatWatchError(error);
3649
+ startupWarnings.push(`teacher_snapshot_reset:${message}`);
3650
+ lastHandledMaterializationPackId = null;
3651
+ log(`Teacher snapshot reset: ${message}`);
3652
+ teacherLoop = createAsyncTeacherLiveLoop(baseTeacherLoopInput);
3653
+ }
3654
+ }
3655
+ if (restoredTeacherState.snapshot !== null && startupWarnings.length === 0) {
3656
+ const restoredSeenExportCount = restoredTeacherState.snapshot.state?.seenExportDigests.length ?? 0;
3657
+ log(`Restored teacher snapshot: seen=${restoredSeenExportCount} artifacts=${restoredTeacherState.snapshot.teacher.artifactCount}`);
3658
+ }
2628
3659
  let restoredCursor = loadWatchSessionTailCursor(sessionTailCursorPath);
2629
3660
  let localSessionTail;
2630
3661
  try {
@@ -2644,8 +3675,18 @@ export async function createWatchCommandRuntime(input) {
2644
3675
  });
2645
3676
  persistWatchSessionTailCursor(sessionTailCursorPath, []);
2646
3677
  }
2647
- let lastHandledMaterializationPackId = loadWatchTeacherSnapshotState(teacherSnapshotPath).lastHandledMaterializationPackId;
2648
- const replayState = await replayWatchScanRootIntoTeacherLoop(teacherLoop, scanRoot);
3678
+ let replayState = {
3679
+ replayedBundleCount: 0,
3680
+ replayedEventCount: 0
3681
+ };
3682
+ try {
3683
+ replayState = await replayWatchScanRootIntoTeacherLoop(teacherLoop, scanRoot);
3684
+ }
3685
+ catch (error) {
3686
+ const message = formatWatchError(error);
3687
+ startupWarnings.push(`teacher_replay_failed:${message}`);
3688
+ log(`Async teacher replay fail-open: ${message}`);
3689
+ }
2649
3690
  if (replayState.replayedBundleCount > 0) {
2650
3691
  log(`Replayed ${replayState.replayedBundleCount} stored export bundle${replayState.replayedBundleCount === 1 ? "" : "s"} (${replayState.replayedEventCount} event${replayState.replayedEventCount === 1 ? "" : "s"})`);
2651
3692
  }
@@ -2656,14 +3697,25 @@ export async function createWatchCommandRuntime(input) {
2656
3697
  log(replayPromotion.logLine);
2657
3698
  bootstrapSnapshot = teacherLoop.snapshot();
2658
3699
  }
3700
+ const bootstrapCursor = localSessionTail.snapshot();
2659
3701
  persistWatchTeacherSnapshot(teacherSnapshotPath, {
3702
+ lastRunAt: bootstrapObservedAt,
2660
3703
  scanRoot,
3704
+ sessionTailCursorPath,
3705
+ sessionTailCursorUpdatedAt: bootstrapObservedAt,
3706
+ sessionTailSessionsTracked: bootstrapCursor.length,
3707
+ sessionTailBridgedEventCount: countWatchCursorBridgedEvents(bootstrapCursor),
3708
+ scannerCheckpointPath: scanner.checkpointPath,
3709
+ scannerCheckpoint: scanner.snapshot(),
2661
3710
  replayedBundleCount: replayState.replayedBundleCount,
2662
3711
  replayedEventCount: replayState.replayedEventCount,
2663
3712
  exportedBundleCount: 0,
2664
3713
  exportedEventCount: 0,
3714
+ startupWarnings,
3715
+ lastTeacherError: null,
2665
3716
  localSessionTailNoopReason: null,
2666
3717
  lastHandledMaterializationPackId,
3718
+ failure: replayPromotion.failure,
2667
3719
  snapshot: bootstrapSnapshot
2668
3720
  });
2669
3721
  return {
@@ -2671,6 +3723,8 @@ export async function createWatchCommandRuntime(input) {
2671
3723
  scanRoot,
2672
3724
  sessionTailCursorPath,
2673
3725
  teacherSnapshotPath,
3726
+ startupWarnings,
3727
+ lastTeacherError: null,
2674
3728
  replayState,
2675
3729
  lastHandledMaterializationPackId,
2676
3730
  scanner,
@@ -2684,6 +3738,7 @@ export async function runWatchCommandPass(runtime, options = {}) {
2684
3738
  const localPoll = runtime.localSessionTail.pollOnce({
2685
3739
  observedAt
2686
3740
  });
3741
+ const scannerCheckpointBeforeScan = runtime.scanner.snapshot();
2687
3742
  const exported = exportLocalSessionTailChangesToScanRoot({
2688
3743
  scanRoot: runtime.scanRoot,
2689
3744
  polledAt: localPoll.polledAt,
@@ -2703,31 +3758,71 @@ export async function runWatchCommandPass(runtime, options = {}) {
2703
3758
  const totalEvents = scanResult.selected.reduce((sum, hit) => sum + hit.eventRange.count, 0);
2704
3759
  let snapshot = runtime.teacherLoop.snapshot();
2705
3760
  let materializedPackId = null;
3761
+ let failure = null;
2706
3762
  if (totalSelected === 0) {
2707
3763
  log("Scanning... no changes");
2708
3764
  }
2709
3765
  else {
2710
3766
  log(`Scanning... ${totalSelected} export bundle${totalSelected === 1 ? "" : "s"} selected, ${totalEvents} event${totalEvents === 1 ? "" : "s"}`);
2711
- const ingestResult = await runtime.teacherLoop.ingestRuntimeEventExportScannerScan(scanResult);
2712
- snapshot = ingestResult.snapshot;
2713
- const promotion = applyWatchMaterialization(runtime.activationRoot, snapshot, runtime.lastHandledMaterializationPackId);
2714
- runtime.lastHandledMaterializationPackId = promotion.lastHandledMaterializationPackId;
2715
- materializedPackId = promotion.materializedPackId;
2716
- if (promotion.logLine !== null) {
2717
- log(promotion.logLine);
3767
+ try {
3768
+ const ingestResult = await runtime.teacherLoop.ingestRuntimeEventExportScannerScan(scanResult);
3769
+ runtime.lastTeacherError = null;
3770
+ snapshot = ingestResult.snapshot;
3771
+ const promotion = applyWatchMaterialization(runtime.activationRoot, snapshot, runtime.lastHandledMaterializationPackId);
3772
+ runtime.lastHandledMaterializationPackId = promotion.lastHandledMaterializationPackId;
3773
+ materializedPackId = promotion.materializedPackId;
3774
+ failure = promotion.failure;
3775
+ if (promotion.logLine !== null) {
3776
+ log(promotion.logLine);
3777
+ snapshot = runtime.teacherLoop.snapshot();
3778
+ }
3779
+ }
3780
+ catch (error) {
3781
+ const message = formatWatchError(error);
3782
+ runtime.lastTeacherError = message;
3783
+ failure = {
3784
+ mode: "teacher_fail_open",
3785
+ detail: message,
3786
+ at: observedAt
3787
+ };
3788
+ log(`Async teacher fail-open: ${message}`);
3789
+ try {
3790
+ runtime.scanner.restoreCheckpoint(scannerCheckpointBeforeScan);
3791
+ }
3792
+ catch (restoreError) {
3793
+ const restoreMessage = formatWatchError(restoreError);
3794
+ runtime.lastTeacherError = `${message}; scanner checkpoint restore failed: ${restoreMessage}`;
3795
+ failure = {
3796
+ mode: "teacher_fail_open",
3797
+ detail: runtime.lastTeacherError,
3798
+ at: observedAt
3799
+ };
3800
+ log(`Scanner checkpoint restore failed: ${restoreMessage}`);
3801
+ }
2718
3802
  snapshot = runtime.teacherLoop.snapshot();
2719
3803
  }
2720
3804
  }
2721
3805
  persistWatchTeacherSnapshot(runtime.teacherSnapshotPath, {
3806
+ lastRunAt: observedAt,
2722
3807
  scanRoot: runtime.scanRoot,
3808
+ sessionTailCursorPath: runtime.sessionTailCursorPath,
3809
+ sessionTailCursorUpdatedAt: observedAt,
3810
+ sessionTailSessionsTracked: localPoll.cursor.length,
3811
+ sessionTailBridgedEventCount: countWatchCursorBridgedEvents(localPoll.cursor),
3812
+ scannerCheckpointPath: runtime.scanner.checkpointPath,
3813
+ scannerCheckpoint: runtime.scanner.snapshot(),
2723
3814
  replayedBundleCount: runtime.replayState.replayedBundleCount,
2724
3815
  replayedEventCount: runtime.replayState.replayedEventCount,
2725
3816
  exportedBundleCount: exported.exportedBundleCount,
2726
3817
  exportedEventCount: exported.exportedEventCount,
3818
+ startupWarnings: runtime.startupWarnings,
3819
+ lastTeacherError: runtime.lastTeacherError,
2727
3820
  localSessionTailNoopReason: localPoll.noopReason,
2728
3821
  lastHandledMaterializationPackId: runtime.lastHandledMaterializationPackId,
3822
+ failure,
2729
3823
  snapshot
2730
3824
  });
3825
+ const persistedScannerCheckpoint = runtime.scanner.snapshot();
2731
3826
  if (options.json) {
2732
3827
  console.log(JSON.stringify({
2733
3828
  timestamp: observedAt,
@@ -2739,6 +3834,10 @@ export async function runWatchCommandPass(runtime, options = {}) {
2739
3834
  events: totalEvents,
2740
3835
  live: scanResult.live.length,
2741
3836
  backfill: scanResult.backfill.length,
3837
+ sessionTailSessionsTracked: localPoll.cursor.length,
3838
+ sessionTailBridgedEvents: countWatchCursorBridgedEvents(localPoll.cursor),
3839
+ scannerProcessedBundles: persistedScannerCheckpoint.processedExportDigests.length,
3840
+ scannerLiveAfter: persistedScannerCheckpoint.live.after?.exportDigest ?? null,
2742
3841
  materialized: materializedPackId,
2743
3842
  diagnostics: snapshot.diagnostics ?? null,
2744
3843
  localSessionTailNoopReason: localPoll.noopReason
@@ -2842,12 +3941,13 @@ function resetActivationRoot(activationRoot) {
2842
3941
  function runResetCommand(parsed) {
2843
3942
  if (parsed.help) {
2844
3943
  console.log([
2845
- "Usage: openclawbrain reset [--activation-root <path>] [--yes] [--json]",
3944
+ "Usage: openclawbrain reset [--activation-root <path>|--openclaw-home <path>] [--yes] [--json]",
2846
3945
  "",
2847
3946
  "Wipes all learned state and returns the brain to seed state.",
2848
3947
  "",
2849
3948
  "Options:",
2850
3949
  " --activation-root <path> Activation root (auto-detected if omitted)",
3950
+ " --openclaw-home <path> Pin auto-detection to one installed OpenClaw profile",
2851
3951
  " --yes, -y Skip confirmation prompt",
2852
3952
  " --json Emit machine-readable JSON output",
2853
3953
  " --help Show this help"
@@ -2898,7 +3998,7 @@ function runResetCommand(parsed) {
2898
3998
  }
2899
3999
  console.log(" Activation pointers reset to seed state.");
2900
4000
  console.log(`\nBrain at ${shortenPath(activationRoot)} is now in seed state.`);
2901
- console.log("Run `openclawbrain status` to verify.");
4001
+ console.log(`Run \`openclawbrain status --activation-root ${quoteShellArg(activationRoot)}\` to verify.`);
2902
4002
  }
2903
4003
  return 0;
2904
4004
  }
@@ -2973,8 +4073,8 @@ export function runOperatorCli(argv = process.argv.slice(2)) {
2973
4073
  });
2974
4074
  return 0;
2975
4075
  }
2976
- if (parsed.command === "install" || parsed.command === "setup") {
2977
- return runSetupCommand(parsed);
4076
+ if (parsed.command === "install") {
4077
+ return runInstallCommand(parsed);
2978
4078
  }
2979
4079
  if (parsed.command === "detach") {
2980
4080
  return runDetachCommand(parsed);
@@ -2983,31 +4083,7 @@ export function runOperatorCli(argv = process.argv.slice(2)) {
2983
4083
  return runUninstallCommand(parsed);
2984
4084
  }
2985
4085
  if (parsed.command === "attach") {
2986
- mkdirSync(parsed.activationRoot, { recursive: true });
2987
- mkdirSync(parsed.packRoot, { recursive: true });
2988
- const result = bootstrapRuntimeAttach({
2989
- profileSelector: "current_profile",
2990
- ...(parsed.brainAttachmentPolicy != null ? { brainAttachmentPolicy: parsed.brainAttachmentPolicy } : {}),
2991
- activationRoot: parsed.activationRoot,
2992
- packRoot: parsed.packRoot,
2993
- packLabel: parsed.packLabel,
2994
- workspace: {
2995
- workspaceId: parsed.workspaceId,
2996
- snapshotId: `${parsed.workspaceId}@bootstrap-${new Date().toISOString().slice(0, 10)}`,
2997
- capturedAt: new Date().toISOString(),
2998
- rootDir: process.cwd(),
2999
- revision: "cli-bootstrap-v1"
3000
- },
3001
- interactionEvents: [],
3002
- feedbackEvents: []
3003
- });
3004
- if (parsed.json) {
3005
- console.log(JSON.stringify(result, null, 2));
3006
- }
3007
- else {
3008
- console.log(formatBootstrapRuntimeAttachReport(result));
3009
- }
3010
- return 0;
4086
+ return runAttachCommand(parsed);
3011
4087
  }
3012
4088
  if (parsed.command === "scan") {
3013
4089
  if (parsed.sessionPath !== null) {
@@ -3044,7 +4120,8 @@ export function runOperatorCli(argv = process.argv.slice(2)) {
3044
4120
  }
3045
4121
  // At this point only status/rollback commands remain
3046
4122
  const statusOrRollback = parsed;
3047
- const activationRoot = requireActivationRoot(statusOrRollback.input, statusOrRollback.command);
4123
+ const activationRoot = requireActivationRoot(statusOrRollback.input, statusOrRollback.openclawHome, statusOrRollback.command);
4124
+ const targetInspection = statusOrRollback.openclawHome === null ? null : inspectOpenClawHome(statusOrRollback.openclawHome);
3048
4125
  if (statusOrRollback.command === "rollback") {
3049
4126
  const result = rollbackRuntimeAttach({
3050
4127
  activationRoot,
@@ -3070,11 +4147,18 @@ export function runOperatorCli(argv = process.argv.slice(2)) {
3070
4147
  }
3071
4148
  else {
3072
4149
  const report = buildOperatorSurfaceReport(operatorInput);
4150
+ const providerConfig = readOpenClawBrainProviderConfig(process.env);
3073
4151
  if (statusOrRollback.detailed) {
3074
- console.log(formatCurrentProfileStatusSummary(status, report));
4152
+ console.log(formatCurrentProfileStatusSummary(status, report, targetInspection, {
4153
+ openclawHome: statusOrRollback.openclawHome,
4154
+ providerConfig
4155
+ }));
3075
4156
  }
3076
4157
  else {
3077
- console.log(formatHumanFriendlyStatus(status, report));
4158
+ console.log(formatHumanFriendlyStatus(status, report, targetInspection, {
4159
+ openclawHome: statusOrRollback.openclawHome,
4160
+ providerConfig
4161
+ }));
3078
4162
  }
3079
4163
  }
3080
4164
  return 0;