@robbiesrobotics/alice-agents 1.1.1 → 1.2.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.
@@ -0,0 +1,2503 @@
1
+ # A.L.I.C.E. Self-Healing System — Technical Specification
2
+
3
+ **Version:** 1.0.0
4
+ **Author:** Dylan (Development Specialist)
5
+ **Date:** 2026-03-16
6
+ **Status:** Production Spec
7
+
8
+ ---
9
+
10
+ ## Table of Contents
11
+
12
+ 1. [Overview](#1-overview)
13
+ 2. [Architecture Summary](#2-architecture-summary)
14
+ 3. [Schema Snapshot Format](#3-schema-snapshot-format)
15
+ 4. [Tool Snapshot Format](#4-tool-snapshot-format)
16
+ 5. [Compatibility Report Format](#5-compatibility-report-format)
17
+ 6. [GitHub Action Workflow](#6-github-action-workflow)
18
+ 7. [Compatibility Checker Script](#7-compatibility-checker-script)
19
+ 8. [Local Remediation Cron](#8-local-remediation-cron)
20
+ 9. [Mission Control UI](#9-mission-control-ui)
21
+ 10. [Patch Generation](#10-patch-generation)
22
+ 11. [Rollback Mechanism](#11-rollback-mechanism)
23
+ 12. [Notification System](#12-notification-system)
24
+ 13. [Error States & Edge Cases](#13-error-states--edge-cases)
25
+ 14. [File Layout Reference](#14-file-layout-reference)
26
+
27
+ ---
28
+
29
+ ## 1. Overview
30
+
31
+ A.L.I.C.E. is a 28-agent framework running on top of OpenClaw. OpenClaw updates may introduce breaking changes to:
32
+
33
+ - `openclaw.json` config schema (field renames, removals, restructuring)
34
+ - Tool API surface (tool names, parameter signatures, tool removals)
35
+ - Behavioral defaults (agent defaults, sandbox semantics, session routing)
36
+ - Skill infrastructure (SKILL.md format, skill loading contract)
37
+
38
+ The Self-Healing System is a two-layer detection and remediation pipeline:
39
+
40
+ | Layer | Where | Trigger | Responsibility |
41
+ |-------|-------|---------|---------------|
42
+ | **Layer 1: Detection** | GitHub CI | Daily + on OpenClaw release | Detect breaking changes, generate reports, publish patches |
43
+ | **Layer 2: Remediation** | User's machine | Every 24h + gateway restart | Apply patches, escalate high-risk changes, rollback on failure |
44
+
45
+ **Design principles:**
46
+ - Auto-fix only low-risk, mechanical changes (renames, value swaps)
47
+ - Always create a backup before mutating files
48
+ - Escalate anything requiring judgment to the user
49
+ - Prefer direct script execution over spawning agents for simple fixes (saves tokens)
50
+ - Maintain an auditable trail of every change made
51
+
52
+ ---
53
+
54
+ ## 2. Architecture Summary
55
+
56
+ ```
57
+ ┌─────────────────────────────────────────────────────────┐
58
+ │ LAYER 1: DETECTION │
59
+ │ (GitHub CI — our repo) │
60
+ │ │
61
+ │ Trigger: daily cron + OpenClaw npm release webhook │
62
+ │ │
63
+ │ ┌──────────────────────────────────────────────────┐ │
64
+ │ │ tools/compatibility-checker.mjs │ │
65
+ │ │ │ │
66
+ │ │ 1. Fetch latest OpenClaw schema + tool registry │ │
67
+ │ │ 2. Diff against schema-snapshot.json │ │
68
+ │ │ 3. Diff against tool-snapshot.json │ │
69
+ │ │ 4. Diff behavioral defaults │ │
70
+ │ │ 5. Diff skill API contract │ │
71
+ │ │ 6. Generate compatibility-report.json │ │
72
+ │ │ 7. If autoFixable → generate patch → npm publi │ │
73
+ │ │ 8. If not fixable → open GitHub issue │ │
74
+ │ └──────────────────────────────────────────────────┘ │
75
+ │ │
76
+ │ Outputs: compatibility/{version}.json (hosted) │
77
+ └─────────────────────────────────────────────────────────┘
78
+
79
+ │ https fetch
80
+
81
+ ┌─────────────────────────────────────────────────────────┐
82
+ │ LAYER 2: REMEDIATION │
83
+ │ (User's machine) │
84
+ │ │
85
+ │ Trigger: OpenClaw cron (24h) + gateway restart hook │
86
+ │ │
87
+ │ ┌──────────────────────────────────────────────────┐ │
88
+ │ │ tools/local-remediation.mjs │ │
89
+ │ │ │ │
90
+ │ │ 1. Read .alice-manifest.json │ │
91
+ │ │ 2. Compare installed vs current OpenClaw ver │ │
92
+ │ │ 3. Fetch compatibility report for new version │ │
93
+ │ │ 4. For each breaking change: │ │
94
+ │ │ - LOW risk → backup → auto-fix → verify │ │
95
+ │ │ - HIGH risk → surface in Mission Control │ │
96
+ │ │ 5. Update manifest │ │
97
+ │ │ 6. Send notifications │ │
98
+ │ └──────────────────────────────────────────────────┘ │
99
+ │ │
100
+ │ Optional: spawn Devon or Avery for complex fixes │
101
+ └─────────────────────────────────────────────────────────┘
102
+ ```
103
+
104
+ ---
105
+
106
+ ## 3. Schema Snapshot Format
107
+
108
+ **File:** `snapshots/schema-snapshot.json`
109
+ **Purpose:** Captures every `openclaw.json` config field that A.L.I.C.E. depends on, at a known-good OpenClaw version.
110
+
111
+ ### 3.1 Format
112
+
113
+ ```json
114
+ {
115
+ "$schema": "https://alice.robbiesrobotics.com/schema/schema-snapshot/v1.json",
116
+ "capturedAt": "2026-03-16T10:00:00Z",
117
+ "openclawVersion": "2026.3.14",
118
+ "aliceVersion": "1.1.1",
119
+ "fields": [
120
+ {
121
+ "path": "agents.list.*.name",
122
+ "type": "string",
123
+ "required": true,
124
+ "description": "Agent identifier used in sessions_spawn calls"
125
+ },
126
+ {
127
+ "path": "agents.list.*.model",
128
+ "type": "string",
129
+ "required": false,
130
+ "description": "Model override for this agent"
131
+ },
132
+ {
133
+ "path": "agents.list.*.tools.allow",
134
+ "type": "array<string>",
135
+ "required": false,
136
+ "description": "Allowlist of tool names for this agent"
137
+ },
138
+ {
139
+ "path": "agents.list.*.tools.deny",
140
+ "type": "array<string>",
141
+ "required": false,
142
+ "description": "Denylist of tool names for this agent"
143
+ },
144
+ {
145
+ "path": "agents.list.*.tools.profile",
146
+ "type": "string",
147
+ "required": false,
148
+ "allowedValues": ["coding", "research", "operations", "minimal"],
149
+ "description": "Tool profile shorthand"
150
+ },
151
+ {
152
+ "path": "agents.defaults.model",
153
+ "type": "string",
154
+ "required": false,
155
+ "description": "Default model for all agents"
156
+ },
157
+ {
158
+ "path": "agents.defaults.sandbox.mode",
159
+ "type": "string",
160
+ "required": false,
161
+ "allowedValues": ["strict", "standard", "permissive"],
162
+ "description": "Default sandbox mode"
163
+ },
164
+ {
165
+ "path": "agents.defaults.heartbeat.interval",
166
+ "type": "number",
167
+ "required": false,
168
+ "description": "Heartbeat interval in seconds"
169
+ },
170
+ {
171
+ "path": "agents.defaults.subagents.maxDepth",
172
+ "type": "number",
173
+ "required": false,
174
+ "description": "Maximum subagent recursion depth"
175
+ },
176
+ {
177
+ "path": "cron.entries",
178
+ "type": "array<object>",
179
+ "required": false,
180
+ "description": "Scheduled cron job definitions"
181
+ },
182
+ {
183
+ "path": "cron.entries.*.schedule",
184
+ "type": "string",
185
+ "required": true,
186
+ "description": "Cron expression"
187
+ },
188
+ {
189
+ "path": "cron.entries.*.command",
190
+ "type": "string",
191
+ "required": true,
192
+ "description": "Command to run on schedule"
193
+ },
194
+ {
195
+ "path": "plugins.entries",
196
+ "type": "array<object>",
197
+ "required": false,
198
+ "description": "Plugin configurations"
199
+ },
200
+ {
201
+ "path": "workspace.root",
202
+ "type": "string",
203
+ "required": false,
204
+ "description": "Root workspace directory"
205
+ },
206
+ {
207
+ "path": "memory.backend",
208
+ "type": "string",
209
+ "required": false,
210
+ "allowedValues": ["sqlite", "postgres", "memory"],
211
+ "description": "Memory storage backend"
212
+ },
213
+ {
214
+ "path": "notifications.channels",
215
+ "type": "array<object>",
216
+ "required": false,
217
+ "description": "Notification channel configurations"
218
+ }
219
+ ],
220
+ "checksum": "sha256:abc123..."
221
+ }
222
+ ```
223
+
224
+ ### 3.2 What to Capture
225
+
226
+ The snapshot must include **every config path** that any of the 28 A.L.I.C.E. agents reads from or writes to, including:
227
+
228
+ - All `agents.list.*` fields used in agent definitions
229
+ - All `agents.defaults.*` fields
230
+ - `cron.*` — for the self-healing cron itself
231
+ - `plugins.*` — for any plugin the agents depend on
232
+ - `workspace.*` — workspace resolution paths
233
+ - `memory.*` — memory backend configuration
234
+ - `notifications.*` — alerting configuration
235
+
236
+ ### 3.3 Update Procedure
237
+
238
+ The snapshot is updated **only when intentionally upgrading** A.L.I.C.E. to support a new OpenClaw version:
239
+
240
+ ```bash
241
+ node tools/compatibility-checker.mjs --update-snapshot --openclaw-version 2026.4.0
242
+ ```
243
+
244
+ This command fetches the new OpenClaw schema, diffs it, applies any changes, and writes a new `schema-snapshot.json` with the updated version and timestamp.
245
+
246
+ ---
247
+
248
+ ## 4. Tool Snapshot Format
249
+
250
+ **File:** `snapshots/tool-snapshot.json`
251
+ **Purpose:** Captures every tool name and parameter signature that A.L.I.C.E. agents reference in their allow/deny lists or call directly.
252
+
253
+ ### 4.1 Format
254
+
255
+ ```json
256
+ {
257
+ "$schema": "https://alice.robbiesrobotics.com/schema/tool-snapshot/v1.json",
258
+ "capturedAt": "2026-03-16T10:00:00Z",
259
+ "openclawVersion": "2026.3.14",
260
+ "aliceVersion": "1.1.1",
261
+ "tools": [
262
+ {
263
+ "name": "read",
264
+ "category": "filesystem",
265
+ "parameters": ["file_path", "path", "offset", "limit"],
266
+ "requiredParameters": [],
267
+ "referencedBy": ["ALL"],
268
+ "criticality": "high"
269
+ },
270
+ {
271
+ "name": "write",
272
+ "category": "filesystem",
273
+ "parameters": ["file_path", "path", "content"],
274
+ "requiredParameters": ["content"],
275
+ "referencedBy": ["dylan", "devon", "avery", "atlas"],
276
+ "criticality": "high"
277
+ },
278
+ {
279
+ "name": "edit",
280
+ "category": "filesystem",
281
+ "parameters": ["file_path", "path", "old_string", "new_string", "oldText", "newText"],
282
+ "requiredParameters": [],
283
+ "referencedBy": ["dylan", "devon", "avery"],
284
+ "criticality": "high"
285
+ },
286
+ {
287
+ "name": "exec",
288
+ "category": "shell",
289
+ "parameters": ["command", "workdir", "timeout", "background", "pty", "env", "elevated", "host", "node", "yieldMs", "security", "ask"],
290
+ "requiredParameters": ["command"],
291
+ "referencedBy": ["dylan", "devon", "avery", "atlas"],
292
+ "criticality": "high"
293
+ },
294
+ {
295
+ "name": "process",
296
+ "category": "shell",
297
+ "parameters": ["action", "sessionId", "data", "keys", "hex", "literal", "text", "timeout", "limit", "offset", "eof", "bracketed"],
298
+ "requiredParameters": ["action"],
299
+ "referencedBy": ["dylan", "devon", "avery"],
300
+ "criticality": "medium"
301
+ },
302
+ {
303
+ "name": "web_search",
304
+ "category": "web",
305
+ "parameters": ["query", "count", "country", "language", "freshness", "date_after", "date_before", "search_lang", "ui_lang"],
306
+ "requiredParameters": ["query"],
307
+ "referencedBy": ["reeve", "scout", "nova", "echo"],
308
+ "criticality": "medium"
309
+ },
310
+ {
311
+ "name": "web_fetch",
312
+ "category": "web",
313
+ "parameters": ["url", "extractMode", "maxChars"],
314
+ "requiredParameters": ["url"],
315
+ "referencedBy": ["reeve", "scout", "nova", "echo"],
316
+ "criticality": "medium"
317
+ },
318
+ {
319
+ "name": "browser",
320
+ "category": "web",
321
+ "parameters": ["action", "url", "selector", "text", "key", "screenshot"],
322
+ "requiredParameters": ["action"],
323
+ "referencedBy": ["reeve", "scout"],
324
+ "criticality": "low"
325
+ },
326
+ {
327
+ "name": "canvas",
328
+ "category": "ui",
329
+ "parameters": ["action", "content", "title", "language"],
330
+ "requiredParameters": ["action"],
331
+ "referencedBy": ["olivia", "nova"],
332
+ "criticality": "low"
333
+ },
334
+ {
335
+ "name": "message",
336
+ "category": "communication",
337
+ "parameters": ["channel", "text", "parse_mode", "reply_to", "buttons"],
338
+ "requiredParameters": ["channel", "text"],
339
+ "referencedBy": ["olivia", "herald"],
340
+ "criticality": "high"
341
+ },
342
+ {
343
+ "name": "tts",
344
+ "category": "communication",
345
+ "parameters": ["text", "voice", "speed"],
346
+ "requiredParameters": ["text"],
347
+ "referencedBy": ["herald", "sable"],
348
+ "criticality": "low"
349
+ },
350
+ {
351
+ "name": "cron",
352
+ "category": "scheduling",
353
+ "parameters": ["action", "id", "schedule", "command", "label"],
354
+ "requiredParameters": ["action"],
355
+ "referencedBy": ["avery", "devon"],
356
+ "criticality": "high"
357
+ },
358
+ {
359
+ "name": "sessions_spawn",
360
+ "category": "agents",
361
+ "parameters": ["agent", "message", "label", "runtime", "channel", "model"],
362
+ "requiredParameters": ["agent", "message"],
363
+ "referencedBy": ["olivia", "avery"],
364
+ "criticality": "critical"
365
+ },
366
+ {
367
+ "name": "sessions_send",
368
+ "category": "agents",
369
+ "parameters": ["session", "message"],
370
+ "requiredParameters": ["session", "message"],
371
+ "referencedBy": ["olivia"],
372
+ "criticality": "critical"
373
+ },
374
+ {
375
+ "name": "sessions_list",
376
+ "category": "agents",
377
+ "parameters": ["filter", "limit"],
378
+ "requiredParameters": [],
379
+ "referencedBy": ["olivia", "atlas"],
380
+ "criticality": "high"
381
+ },
382
+ {
383
+ "name": "sessions_history",
384
+ "category": "agents",
385
+ "parameters": ["session", "limit", "offset"],
386
+ "requiredParameters": ["session"],
387
+ "referencedBy": ["olivia", "atlas"],
388
+ "criticality": "medium"
389
+ },
390
+ {
391
+ "name": "session_status",
392
+ "category": "agents",
393
+ "parameters": [],
394
+ "requiredParameters": [],
395
+ "referencedBy": ["ALL"],
396
+ "criticality": "medium"
397
+ },
398
+ {
399
+ "name": "agents_list",
400
+ "category": "agents",
401
+ "parameters": ["filter"],
402
+ "requiredParameters": [],
403
+ "referencedBy": ["olivia", "atlas"],
404
+ "criticality": "high"
405
+ },
406
+ {
407
+ "name": "apply_patch",
408
+ "category": "filesystem",
409
+ "parameters": ["patch", "workdir"],
410
+ "requiredParameters": ["patch"],
411
+ "referencedBy": ["dylan", "devon"],
412
+ "criticality": "medium"
413
+ },
414
+ {
415
+ "name": "memory_search",
416
+ "category": "memory",
417
+ "parameters": ["query", "limit", "agent", "since"],
418
+ "requiredParameters": ["query"],
419
+ "referencedBy": ["ALL"],
420
+ "criticality": "high"
421
+ },
422
+ {
423
+ "name": "memory_get",
424
+ "category": "memory",
425
+ "parameters": ["id"],
426
+ "requiredParameters": ["id"],
427
+ "referencedBy": ["ALL"],
428
+ "criticality": "high"
429
+ }
430
+ ],
431
+ "agentToolMap": {
432
+ "olivia": ["read", "write", "sessions_spawn", "sessions_send", "sessions_list", "sessions_history", "session_status", "agents_list", "message", "canvas", "memory_search", "memory_get"],
433
+ "dylan": ["read", "write", "edit", "exec", "process", "apply_patch", "web_search", "web_fetch", "memory_search", "memory_get"],
434
+ "devon": ["read", "write", "edit", "exec", "process", "apply_patch", "cron", "sessions_spawn", "memory_search", "memory_get"],
435
+ "avery": ["read", "write", "edit", "exec", "cron", "sessions_spawn", "web_search", "memory_search", "memory_get"],
436
+ "ALL": ["read", "session_status", "memory_search", "memory_get"]
437
+ },
438
+ "checksum": "sha256:def456..."
439
+ }
440
+ ```
441
+
442
+ ### 4.2 Criticality Levels
443
+
444
+ | Level | Meaning |
445
+ |-------|---------|
446
+ | `critical` | Removal would break core orchestration (sessions_spawn, sessions_send) |
447
+ | `high` | Removal would break most agents significantly |
448
+ | `medium` | Removal degrades capability but workarounds exist |
449
+ | `low` | Removal is inconvenient but non-blocking |
450
+
451
+ ---
452
+
453
+ ## 5. Compatibility Report Format
454
+
455
+ **File:** `compatibility/{openclawVersion}.json`
456
+ **Hosted at:** `https://raw.githubusercontent.com/robbiesrobotics-bot/alice/main/compatibility/{version}.json`
457
+
458
+ ### 5.1 Full Schema
459
+
460
+ ```json
461
+ {
462
+ "$schema": "https://alice.robbiesrobotics.com/schema/compatibility-report/v1.json",
463
+ "openclawVersion": "2026.4.0",
464
+ "aliceVersion": "1.1.1",
465
+ "reportGeneratedAt": "2026-04-01T08:30:00Z",
466
+ "compatible": false,
467
+ "summary": {
468
+ "breakingChangesCount": 3,
469
+ "warningsCount": 1,
470
+ "autoFixableCount": 2,
471
+ "manualReviewCount": 1
472
+ },
473
+ "breakingChanges": [
474
+ {
475
+ "id": "BC-2026.4.0-001",
476
+ "category": "config",
477
+ "severity": "low",
478
+ "autoFixable": true,
479
+ "field": "agents.list.*.tools.profile",
480
+ "change": "Enum value 'coding' renamed to 'developer'",
481
+ "description": "The tool profile shorthand 'coding' has been renamed to 'developer' in OpenClaw 2026.4.0. All agent configs using profile: 'coding' must be updated.",
482
+ "affectedAgents": ["dylan", "devon"],
483
+ "fix": {
484
+ "type": "value-rename",
485
+ "target": "openclaw.json",
486
+ "jsonPath": "$.agents.list[*].tools.profile",
487
+ "from": "coding",
488
+ "to": "developer"
489
+ },
490
+ "rollbackable": true,
491
+ "verificationCommand": "openclaw validate --config openclaw.json"
492
+ },
493
+ {
494
+ "id": "BC-2026.4.0-002",
495
+ "category": "tool",
496
+ "severity": "low",
497
+ "autoFixable": true,
498
+ "field": "tools.web_fetch.parameters.extractMode",
499
+ "change": "Parameter 'extractMode' default changed from 'markdown' to 'text'",
500
+ "description": "The web_fetch tool now defaults to 'text' extraction mode. A.L.I.C.E. agents that rely on markdown output must pass extractMode: 'markdown' explicitly.",
501
+ "affectedAgents": ["reeve", "scout", "nova", "echo"],
502
+ "fix": {
503
+ "type": "config-inject",
504
+ "target": "openclaw.json",
505
+ "jsonPath": "$.agents.defaults.toolDefaults.web_fetch",
506
+ "value": { "extractMode": "markdown" }
507
+ },
508
+ "rollbackable": true,
509
+ "verificationCommand": null
510
+ },
511
+ {
512
+ "id": "BC-2026.4.0-003",
513
+ "category": "behavioral",
514
+ "severity": "high",
515
+ "autoFixable": false,
516
+ "field": "agents.defaults.sandbox.mode",
517
+ "change": "Default sandbox mode changed from 'standard' to 'strict'. Tool 'exec' now requires explicit 'security: full' to run arbitrary shell commands.",
518
+ "description": "OpenClaw 2026.4.0 tightens sandbox defaults. 'exec' calls without a security parameter will now be blocked under strict mode. A.L.I.C.E. agents that use exec must be audited and updated individually. This requires human review because blanket-patching all agents to 'security: full' would defeat the security intent.",
519
+ "affectedAgents": ["dylan", "devon", "avery", "atlas"],
520
+ "fix": null,
521
+ "manualSteps": [
522
+ "Review each affected agent's exec usage in their SOUL.md and config",
523
+ "For trusted agents (dylan, devon): add security: 'full' to their agent config",
524
+ "For limited agents (avery, atlas): add security: 'allowlist' with explicit allowed commands",
525
+ "Test each agent's exec capability after update",
526
+ "Update schema-snapshot.json to reflect new sandbox.mode default"
527
+ ],
528
+ "rollbackable": false,
529
+ "githubIssueUrl": "https://github.com/robbiesrobotics-bot/alice/issues/142",
530
+ "verificationCommand": "node tools/verify-sandbox.mjs"
531
+ }
532
+ ],
533
+ "warnings": [
534
+ {
535
+ "id": "W-2026.4.0-001",
536
+ "category": "skills",
537
+ "severity": "low",
538
+ "field": "skills.discovery.path",
539
+ "change": "Skills are now also discovered from ~/.local/share/openclaw/skills in addition to node_modules",
540
+ "description": "Non-breaking, but A.L.I.C.E. skill references should be reviewed to ensure no naming conflicts with user-installed skills.",
541
+ "affectedAgents": [],
542
+ "actionRequired": false
543
+ }
544
+ ],
545
+ "patches": [
546
+ {
547
+ "changeId": "BC-2026.4.0-001",
548
+ "patchType": "json-transform",
549
+ "targetFile": "openclaw.json",
550
+ "operations": [
551
+ {
552
+ "op": "replace-value",
553
+ "jsonPath": "$.agents.list[?(@.tools.profile=='coding')].tools.profile",
554
+ "value": "developer"
555
+ }
556
+ ]
557
+ },
558
+ {
559
+ "changeId": "BC-2026.4.0-002",
560
+ "patchType": "json-transform",
561
+ "targetFile": "openclaw.json",
562
+ "operations": [
563
+ {
564
+ "op": "set",
565
+ "jsonPath": "$.agents.defaults.toolDefaults.web_fetch.extractMode",
566
+ "value": "markdown"
567
+ }
568
+ ]
569
+ }
570
+ ],
571
+ "alicePatchVersion": "1.1.2",
572
+ "alicePatchNpmTag": "@robbiesrobotics/alice-agents@1.1.2"
573
+ }
574
+ ```
575
+
576
+ ### 5.2 Severity Matrix
577
+
578
+ | Severity | Auto-Fix? | User Alert? | Block? |
579
+ |----------|-----------|-------------|--------|
580
+ | `low` | Yes | Notification only | No |
581
+ | `medium` | Yes (with confirmation) | Mission Control + notification | No |
582
+ | `high` | No | Mission Control + notification | No (warn) |
583
+ | `critical` | No | Mission Control + notification + Telegram | Yes (gate) |
584
+
585
+ ---
586
+
587
+ ## 6. GitHub Action Workflow
588
+
589
+ **File:** `.github/workflows/compatibility-check.yml`
590
+
591
+ ```yaml
592
+ name: OpenClaw Compatibility Check
593
+
594
+ on:
595
+ schedule:
596
+ # Run daily at 06:00 UTC
597
+ - cron: '0 6 * * *'
598
+
599
+ # Triggered by OpenClaw npm release webhook
600
+ repository_dispatch:
601
+ types: [openclaw-release]
602
+
603
+ # Allow manual trigger for testing
604
+ workflow_dispatch:
605
+ inputs:
606
+ openclaw_version:
607
+ description: 'Specific OpenClaw version to check (leave blank for latest)'
608
+ required: false
609
+ type: string
610
+ dry_run:
611
+ description: 'Dry run (generate report but do not publish)'
612
+ required: false
613
+ type: boolean
614
+ default: false
615
+
616
+ env:
617
+ NODE_VERSION: '22'
618
+ ALICE_REPO: 'robbiesrobotics-bot/alice'
619
+
620
+ jobs:
621
+ detect-openclaw-version:
622
+ name: Detect Latest OpenClaw Version
623
+ runs-on: ubuntu-latest
624
+ outputs:
625
+ openclaw_version: ${{ steps.version.outputs.version }}
626
+ alice_version: ${{ steps.alice_version.outputs.version }}
627
+ already_checked: ${{ steps.cache-check.outputs.cache-hit }}
628
+
629
+ steps:
630
+ - name: Checkout A.L.I.C.E. repo
631
+ uses: actions/checkout@v4
632
+
633
+ - name: Setup Node.js
634
+ uses: actions/setup-node@v4
635
+ with:
636
+ node-version: ${{ env.NODE_VERSION }}
637
+
638
+ - name: Determine OpenClaw version to check
639
+ id: version
640
+ run: |
641
+ if [ -n "${{ github.event.inputs.openclaw_version }}" ]; then
642
+ VERSION="${{ github.event.inputs.openclaw_version }}"
643
+ elif [ "${{ github.event_name }}" = "repository_dispatch" ]; then
644
+ VERSION="${{ github.event.client_payload.version }}"
645
+ else
646
+ VERSION=$(npm view openclaw version)
647
+ fi
648
+ echo "version=$VERSION" >> $GITHUB_OUTPUT
649
+ echo "Checking OpenClaw version: $VERSION"
650
+
651
+ - name: Get A.L.I.C.E. version
652
+ id: alice_version
653
+ run: |
654
+ VERSION=$(node -e "console.log(require('./package.json').version)")
655
+ echo "version=$VERSION" >> $GITHUB_OUTPUT
656
+
657
+ - name: Check if this version was already analyzed
658
+ id: cache-check
659
+ uses: actions/cache@v4
660
+ with:
661
+ path: compatibility/${{ steps.version.outputs.version }}.json
662
+ key: compat-${{ steps.version.outputs.version }}-${{ steps.alice_version.outputs.version }}
663
+
664
+ run-compatibility-check:
665
+ name: Run Compatibility Checker
666
+ runs-on: ubuntu-latest
667
+ needs: detect-openclaw-version
668
+ if: needs.detect-openclaw-version.outputs.already_checked != 'true'
669
+
670
+ outputs:
671
+ compatible: ${{ steps.check.outputs.compatible }}
672
+ auto_fixable_only: ${{ steps.check.outputs.auto_fixable_only }}
673
+ breaking_count: ${{ steps.check.outputs.breaking_count }}
674
+ patch_version: ${{ steps.check.outputs.patch_version }}
675
+
676
+ steps:
677
+ - name: Checkout A.L.I.C.E. repo
678
+ uses: actions/checkout@v4
679
+ with:
680
+ token: ${{ secrets.GITHUB_TOKEN }}
681
+
682
+ - name: Setup Node.js
683
+ uses: actions/setup-node@v4
684
+ with:
685
+ node-version: ${{ env.NODE_VERSION }}
686
+ registry-url: 'https://registry.npmjs.org'
687
+
688
+ - name: Install A.L.I.C.E. dependencies
689
+ run: npm ci
690
+
691
+ - name: Install target OpenClaw version
692
+ run: |
693
+ npm install openclaw@${{ needs.detect-openclaw-version.outputs.openclaw_version }} --no-save
694
+ echo "Installed OpenClaw ${{ needs.detect-openclaw-version.outputs.openclaw_version }}"
695
+
696
+ - name: Run compatibility checker
697
+ id: check
698
+ run: |
699
+ node tools/compatibility-checker.mjs \
700
+ --openclaw-version "${{ needs.detect-openclaw-version.outputs.openclaw_version }}" \
701
+ --alice-version "${{ needs.detect-openclaw-version.outputs.alice_version }}" \
702
+ --output "compatibility/${{ needs.detect-openclaw-version.outputs.openclaw_version }}.json" \
703
+ --snapshots-dir "snapshots/"
704
+
705
+ # Parse outputs for downstream steps
706
+ COMPATIBLE=$(node -e "const r=require('./compatibility/${{ needs.detect-openclaw-version.outputs.openclaw_version }}.json'); console.log(r.compatible)")
707
+ BREAKING=$(node -e "const r=require('./compatibility/${{ needs.detect-openclaw-version.outputs.openclaw_version }}.json'); console.log(r.summary.breakingChangesCount)")
708
+ AUTO_ONLY=$(node -e "const r=require('./compatibility/${{ needs.detect-openclaw-version.outputs.openclaw_version }}.json'); console.log(r.summary.breakingChangesCount > 0 && r.summary.manualReviewCount === 0)")
709
+ PATCH_VER=$(node -e "const r=require('./compatibility/${{ needs.detect-openclaw-version.outputs.openclaw_version }}.json'); console.log(r.alicePatchVersion || '')")
710
+
711
+ echo "compatible=$COMPATIBLE" >> $GITHUB_OUTPUT
712
+ echo "breaking_count=$BREAKING" >> $GITHUB_OUTPUT
713
+ echo "auto_fixable_only=$AUTO_ONLY" >> $GITHUB_OUTPUT
714
+ echo "patch_version=$PATCH_VER" >> $GITHUB_OUTPUT
715
+
716
+ - name: Commit compatibility report
717
+ if: github.event.inputs.dry_run != 'true'
718
+ run: |
719
+ git config user.name "alice-bot"
720
+ git config user.email "alice-bot@robbiesrobotics.com"
721
+ git add compatibility/
722
+ git diff --cached --quiet || git commit -m "chore: add compatibility report for OpenClaw ${{ needs.detect-openclaw-version.outputs.openclaw_version }}"
723
+ git push
724
+
725
+ publish-auto-patch:
726
+ name: Publish Auto-Fix Patch
727
+ runs-on: ubuntu-latest
728
+ needs: [detect-openclaw-version, run-compatibility-check]
729
+ if: |
730
+ needs.run-compatibility-check.outputs.compatible == 'false' &&
731
+ needs.run-compatibility-check.outputs.auto_fixable_only == 'true' &&
732
+ github.event.inputs.dry_run != 'true'
733
+
734
+ steps:
735
+ - name: Checkout A.L.I.C.E. repo
736
+ uses: actions/checkout@v4
737
+ with:
738
+ token: ${{ secrets.GITHUB_TOKEN }}
739
+
740
+ - name: Setup Node.js
741
+ uses: actions/setup-node@v4
742
+ with:
743
+ node-version: ${{ env.NODE_VERSION }}
744
+ registry-url: 'https://registry.npmjs.org'
745
+
746
+ - name: Install dependencies
747
+ run: npm ci
748
+
749
+ - name: Apply auto-fix patches to package
750
+ run: |
751
+ node tools/compatibility-checker.mjs \
752
+ --apply-patches \
753
+ --report "compatibility/${{ needs.detect-openclaw-version.outputs.openclaw_version }}.json" \
754
+ --target-version "${{ needs.run-compatibility-check.outputs.patch_version }}"
755
+
756
+ - name: Bump version
757
+ run: |
758
+ npm version ${{ needs.run-compatibility-check.outputs.patch_version }} --no-git-tag-version
759
+
760
+ - name: Publish to npm
761
+ run: npm publish --access public
762
+ env:
763
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
764
+
765
+ - name: Commit version bump
766
+ run: |
767
+ git config user.name "alice-bot"
768
+ git config user.email "alice-bot@robbiesrobotics.com"
769
+ git add package.json package-lock.json
770
+ git commit -m "chore: bump to ${{ needs.run-compatibility-check.outputs.patch_version }} for OpenClaw ${{ needs.detect-openclaw-version.outputs.openclaw_version }} compatibility"
771
+ git tag "v${{ needs.run-compatibility-check.outputs.patch_version }}"
772
+ git push --follow-tags
773
+
774
+ open-manual-review-issue:
775
+ name: Open GitHub Issue for Manual Changes
776
+ runs-on: ubuntu-latest
777
+ needs: [detect-openclaw-version, run-compatibility-check]
778
+ if: |
779
+ needs.run-compatibility-check.outputs.compatible == 'false' &&
780
+ needs.run-compatibility-check.outputs.auto_fixable_only == 'false'
781
+
782
+ steps:
783
+ - name: Checkout A.L.I.C.E. repo
784
+ uses: actions/checkout@v4
785
+
786
+ - name: Setup Node.js
787
+ uses: actions/setup-node@v4
788
+ with:
789
+ node-version: ${{ env.NODE_VERSION }}
790
+
791
+ - name: Generate issue body
792
+ id: issue
793
+ run: |
794
+ BODY=$(node tools/compatibility-checker.mjs \
795
+ --generate-issue-body \
796
+ --report "compatibility/${{ needs.detect-openclaw-version.outputs.openclaw_version }}.json")
797
+ echo "body<<EOF" >> $GITHUB_OUTPUT
798
+ echo "$BODY" >> $GITHUB_OUTPUT
799
+ echo "EOF" >> $GITHUB_OUTPUT
800
+
801
+ - name: Create GitHub issue
802
+ uses: actions/github-script@v7
803
+ with:
804
+ script: |
805
+ const body = `${{ steps.issue.outputs.body }}`;
806
+ await github.rest.issues.create({
807
+ owner: context.repo.owner,
808
+ repo: context.repo.repo,
809
+ title: '🚨 Manual review required: OpenClaw ${{ needs.detect-openclaw-version.outputs.openclaw_version }} breaking changes',
810
+ body: body,
811
+ labels: ['compatibility', 'breaking-change', 'needs-review']
812
+ });
813
+
814
+ notify-telegram:
815
+ name: Send Telegram Notification
816
+ runs-on: ubuntu-latest
817
+ needs: [detect-openclaw-version, run-compatibility-check, publish-auto-patch, open-manual-review-issue]
818
+ if: always() && needs.run-compatibility-check.result == 'success'
819
+
820
+ steps:
821
+ - name: Notify via Telegram
822
+ run: |
823
+ OCVER="${{ needs.detect-openclaw-version.outputs.openclaw_version }}"
824
+ COMPATIBLE="${{ needs.run-compatibility-check.outputs.compatible }}"
825
+ BREAKING="${{ needs.run-compatibility-check.outputs.breaking_count }}"
826
+
827
+ if [ "$COMPATIBLE" = "true" ]; then
828
+ MSG="✅ OpenClaw $OCVER is fully compatible with A.L.I.C.E. No changes needed."
829
+ elif [ "${{ needs.publish-auto-patch.result }}" = "success" ]; then
830
+ MSG="🔧 OpenClaw $OCVER detected $BREAKING change(s). A.L.I.C.E. patch published automatically. Run: npm update @robbiesrobotics/alice-agents"
831
+ else
832
+ MSG="⚠️ OpenClaw $OCVER has breaking changes that need manual review. Check GitHub Issues or Mission Control → Settings → Compatibility."
833
+ fi
834
+
835
+ curl -s -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \
836
+ -d chat_id="${{ secrets.TELEGRAM_CHAT_ID }}" \
837
+ -d text="$MSG" \
838
+ -d parse_mode="HTML"
839
+ ```
840
+
841
+ ---
842
+
843
+ ## 7. Compatibility Checker Script
844
+
845
+ **File:** `tools/compatibility-checker.mjs`
846
+
847
+ ### 7.1 What It Does (Step by Step)
848
+
849
+ ```
850
+ 1. Parse CLI arguments
851
+ 2. Load snapshots (schema-snapshot.json, tool-snapshot.json)
852
+ 3. Extract OpenClaw schema from installed package
853
+ 4. Run 4 category checks (config, tools, behavioral, skills)
854
+ 5. Build compatibility report
855
+ 6. Write report to --output path
856
+ 7. If --apply-patches: apply all autoFixable patches to package files
857
+ 8. If --generate-issue-body: render markdown for GitHub issue
858
+ 9. Exit with code 0 (compatible) or 1 (breaking changes found)
859
+ ```
860
+
861
+ ### 7.2 Full Script
862
+
863
+ ```javascript
864
+ #!/usr/bin/env node
865
+ // tools/compatibility-checker.mjs
866
+ // A.L.I.C.E. Self-Healing System — Compatibility Checker
867
+ // Runs in GitHub CI to detect OpenClaw breaking changes.
868
+
869
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
870
+ import { createHash } from 'crypto';
871
+ import { execSync } from 'child_process';
872
+ import { resolve, dirname } from 'path';
873
+ import { fileURLToPath } from 'url';
874
+
875
+ const __dirname = dirname(fileURLToPath(import.meta.url));
876
+ const ROOT = resolve(__dirname, '..');
877
+
878
+ // ─────────────────────────────────────────────
879
+ // CLI argument parsing
880
+ // ─────────────────────────────────────────────
881
+ const args = process.argv.slice(2);
882
+ const getArg = (name, def = null) => {
883
+ const idx = args.indexOf(name);
884
+ return idx !== -1 ? args[idx + 1] : def;
885
+ };
886
+ const hasFlag = (name) => args.includes(name);
887
+
888
+ const openclawVersion = getArg('--openclaw-version');
889
+ const aliceVersion = getArg('--alice-version');
890
+ const outputPath = getArg('--output');
891
+ const snapshotsDir = getArg('--snapshots-dir', 'snapshots/');
892
+ const reportPath = getArg('--report');
893
+ const targetVersion = getArg('--target-version');
894
+ const MODE_APPLY_PATCHES = hasFlag('--apply-patches');
895
+ const MODE_GENERATE_ISSUE = hasFlag('--generate-issue-body');
896
+ const MODE_UPDATE_SNAPSHOT = hasFlag('--update-snapshot');
897
+
898
+ // ─────────────────────────────────────────────
899
+ // Helpers
900
+ // ─────────────────────────────────────────────
901
+ function load(path) {
902
+ const full = resolve(ROOT, path);
903
+ if (!existsSync(full)) throw new Error(`File not found: ${full}`);
904
+ return JSON.parse(readFileSync(full, 'utf8'));
905
+ }
906
+
907
+ function save(path, data) {
908
+ writeFileSync(resolve(ROOT, path), JSON.stringify(data, null, 2) + '\n', 'utf8');
909
+ }
910
+
911
+ function sha256(obj) {
912
+ return 'sha256:' + createHash('sha256').update(JSON.stringify(obj)).digest('hex').slice(0, 16);
913
+ }
914
+
915
+ function jsonPath(obj, path) {
916
+ // Simple dot-bracket path resolver (no regex wildcards at runtime)
917
+ return path.split('.').reduce((acc, key) => {
918
+ if (acc === undefined) return undefined;
919
+ return acc[key];
920
+ }, obj);
921
+ }
922
+
923
+ // ─────────────────────────────────────────────
924
+ // Step 1: Load snapshots
925
+ // ─────────────────────────────────────────────
926
+ function loadSnapshots(dir) {
927
+ return {
928
+ schema: load(`${dir}schema-snapshot.json`),
929
+ tools: load(`${dir}tool-snapshot.json`),
930
+ };
931
+ }
932
+
933
+ // ─────────────────────────────────────────────
934
+ // Step 2: Extract OpenClaw schema from installed package
935
+ // ─────────────────────────────────────────────
936
+ function extractOpenclawSchema() {
937
+ // Try to load OpenClaw's exported schema
938
+ // OpenClaw exposes its config schema via package exports
939
+ let openclawPkg;
940
+ try {
941
+ openclawPkg = load('node_modules/openclaw/package.json');
942
+ } catch {
943
+ throw new Error('openclaw not found in node_modules. Run: npm install openclaw@latest --no-save');
944
+ }
945
+
946
+ // Load OpenClaw's config schema definition
947
+ let configSchema;
948
+ const schemaPath = openclawPkg.exports?.['./config-schema'] || 'dist/config.schema.json';
949
+ try {
950
+ configSchema = load(`node_modules/openclaw/${schemaPath}`);
951
+ } catch {
952
+ // Fallback: extract from source if schema file not found
953
+ console.warn('Config schema file not found, attempting source extraction...');
954
+ configSchema = extractSchemaFromSource();
955
+ }
956
+
957
+ // Load OpenClaw's tool registry
958
+ let toolRegistry;
959
+ const toolsPath = openclawPkg.exports?.['./tools'] || 'dist/tools.json';
960
+ try {
961
+ toolRegistry = load(`node_modules/openclaw/${toolsPath}`);
962
+ } catch {
963
+ toolRegistry = extractToolsFromSource();
964
+ }
965
+
966
+ return { configSchema, toolRegistry, version: openclawPkg.version };
967
+ }
968
+
969
+ function extractSchemaFromSource() {
970
+ // Parse OpenClaw source files to extract config fields
971
+ // This is a fallback when no compiled schema is available
972
+ const result = { fields: {}, defaults: {} };
973
+
974
+ // Check known source locations
975
+ const candidates = [
976
+ 'node_modules/openclaw/src/config/schema.ts',
977
+ 'node_modules/openclaw/src/config.ts',
978
+ 'node_modules/openclaw/lib/config.js',
979
+ ];
980
+
981
+ for (const candidate of candidates) {
982
+ const full = resolve(ROOT, candidate);
983
+ if (existsSync(full)) {
984
+ const src = readFileSync(full, 'utf8');
985
+ // Extract Zod or JSONSchema definitions via regex
986
+ const fieldMatches = src.matchAll(/['"]([a-zA-Z][a-zA-Z0-9._*]+)['"]\s*:/g);
987
+ for (const [, field] of fieldMatches) {
988
+ result.fields[field] = { type: 'unknown', source: candidate };
989
+ }
990
+ break;
991
+ }
992
+ }
993
+
994
+ return result;
995
+ }
996
+
997
+ function extractToolsFromSource() {
998
+ // Parse OpenClaw tool definitions
999
+ const result = { tools: [] };
1000
+ const candidates = [
1001
+ 'node_modules/openclaw/src/tools/index.ts',
1002
+ 'node_modules/openclaw/src/tools.ts',
1003
+ 'node_modules/openclaw/lib/tools.js',
1004
+ ];
1005
+
1006
+ for (const candidate of candidates) {
1007
+ const full = resolve(ROOT, candidate);
1008
+ if (existsSync(full)) {
1009
+ const src = readFileSync(full, 'utf8');
1010
+ const nameMatches = src.matchAll(/name:\s*['"]([a-z_]+)['"]/g);
1011
+ for (const [, name] of nameMatches) {
1012
+ result.tools.push({ name, parameters: [] });
1013
+ }
1014
+ break;
1015
+ }
1016
+ }
1017
+
1018
+ return result;
1019
+ }
1020
+
1021
+ // ─────────────────────────────────────────────
1022
+ // Step 3: Config schema diff
1023
+ // ─────────────────────────────────────────────
1024
+ function diffConfigSchema(snapshot, openclawSchema) {
1025
+ const changes = [];
1026
+ const snapshotFields = new Map(snapshot.fields.map(f => [f.path, f]));
1027
+
1028
+ // Build a flat field map from OpenClaw's schema
1029
+ const openclawFields = buildFieldMap(openclawSchema);
1030
+
1031
+ for (const [path, snapshotField] of snapshotFields) {
1032
+ const current = openclawFields.get(path);
1033
+
1034
+ if (!current) {
1035
+ // Field was removed or renamed
1036
+ changes.push({
1037
+ id: `config-removed-${path.replace(/[^a-z0-9]/gi, '-')}`,
1038
+ category: 'config',
1039
+ type: 'field-removed',
1040
+ field: path,
1041
+ change: `Field '${path}' was removed from OpenClaw config schema`,
1042
+ severity: snapshotField.required ? 'high' : 'low',
1043
+ autoFixable: false,
1044
+ });
1045
+ continue;
1046
+ }
1047
+
1048
+ // Check type changes
1049
+ if (current.type !== snapshotField.type) {
1050
+ changes.push({
1051
+ id: `config-type-${path.replace(/[^a-z0-9]/gi, '-')}`,
1052
+ category: 'config',
1053
+ type: 'type-changed',
1054
+ field: path,
1055
+ change: `Type changed from '${snapshotField.type}' to '${current.type}'`,
1056
+ severity: 'medium',
1057
+ autoFixable: false,
1058
+ });
1059
+ }
1060
+
1061
+ // Check allowed values changes
1062
+ if (snapshotField.allowedValues && current.allowedValues) {
1063
+ const removed = snapshotField.allowedValues.filter(v => !current.allowedValues.includes(v));
1064
+ const added = current.allowedValues.filter(v => !snapshotField.allowedValues.includes(v));
1065
+
1066
+ if (removed.length > 0) {
1067
+ // Check if this looks like a rename (removed 1, added 1)
1068
+ if (removed.length === 1 && added.length === 1) {
1069
+ changes.push({
1070
+ id: `config-rename-${path.replace(/[^a-z0-9]/gi, '-')}-${removed[0]}`,
1071
+ category: 'config',
1072
+ type: 'value-renamed',
1073
+ field: path,
1074
+ change: `Value '${removed[0]}' renamed to '${added[0]}'`,
1075
+ severity: 'low',
1076
+ autoFixable: true,
1077
+ fix: {
1078
+ type: 'value-rename',
1079
+ target: 'openclaw.json',
1080
+ jsonPath: `$.agents.list[*].${path.split('.').pop()}`,
1081
+ from: removed[0],
1082
+ to: added[0],
1083
+ },
1084
+ });
1085
+ } else {
1086
+ changes.push({
1087
+ id: `config-values-${path.replace(/[^a-z0-9]/gi, '-')}`,
1088
+ category: 'config',
1089
+ type: 'allowed-values-changed',
1090
+ field: path,
1091
+ change: `Values removed: [${removed.join(', ')}], Values added: [${added.join(', ')}]`,
1092
+ severity: 'medium',
1093
+ autoFixable: false,
1094
+ });
1095
+ }
1096
+ }
1097
+ }
1098
+ }
1099
+
1100
+ // Check for new required fields
1101
+ for (const [path, field] of openclawFields) {
1102
+ if (field.required && !snapshotFields.has(path)) {
1103
+ changes.push({
1104
+ id: `config-new-required-${path.replace(/[^a-z0-9]/gi, '-')}`,
1105
+ category: 'config',
1106
+ type: 'new-required-field',
1107
+ field: path,
1108
+ change: `New required field '${path}' added to OpenClaw config schema`,
1109
+ severity: 'high',
1110
+ autoFixable: false,
1111
+ });
1112
+ }
1113
+ }
1114
+
1115
+ return changes;
1116
+ }
1117
+
1118
+ function buildFieldMap(schema) {
1119
+ const map = new Map();
1120
+ // Support both JSONSchema and OpenClaw's custom schema format
1121
+ if (schema.fields) {
1122
+ for (const f of schema.fields) map.set(f.path, f);
1123
+ } else if (schema.properties) {
1124
+ flattenProperties(schema.properties, '', map);
1125
+ }
1126
+ return map;
1127
+ }
1128
+
1129
+ function flattenProperties(props, prefix, map) {
1130
+ for (const [key, val] of Object.entries(props)) {
1131
+ const path = prefix ? `${prefix}.${key}` : key;
1132
+ map.set(path, { path, type: val.type || 'unknown', required: false });
1133
+ if (val.properties) flattenProperties(val.properties, path, map);
1134
+ }
1135
+ }
1136
+
1137
+ // ─────────────────────────────────────────────
1138
+ // Step 4: Tool registry diff
1139
+ // ─────────────────────────────────────────────
1140
+ function diffToolRegistry(snapshot, openclawTools) {
1141
+ const changes = [];
1142
+ const snapshotTools = new Map(snapshot.tools.map(t => [t.name, t]));
1143
+ const currentTools = new Map();
1144
+
1145
+ // Build current tool map from OpenClaw
1146
+ const toolList = openclawTools.tools || openclawTools;
1147
+ for (const tool of (Array.isArray(toolList) ? toolList : [])) {
1148
+ currentTools.set(tool.name, tool);
1149
+ }
1150
+
1151
+ for (const [name, snapshotTool] of snapshotTools) {
1152
+ const current = currentTools.get(name);
1153
+
1154
+ if (!current) {
1155
+ changes.push({
1156
+ id: `tool-removed-${name}`,
1157
+ category: 'tool',
1158
+ type: 'tool-removed',
1159
+ field: `tools.${name}`,
1160
+ change: `Tool '${name}' was removed from OpenClaw`,
1161
+ severity: snapshotTool.criticality === 'critical' ? 'critical' :
1162
+ snapshotTool.criticality === 'high' ? 'high' : 'medium',
1163
+ autoFixable: false,
1164
+ affectedAgents: snapshotTool.referencedBy,
1165
+ });
1166
+ continue;
1167
+ }
1168
+
1169
+ // Check parameter changes
1170
+ const snapshotParams = new Set(snapshotTool.parameters || []);
1171
+ const currentParams = new Set(current.parameters?.map(p =>
1172
+ typeof p === 'string' ? p : p.name
1173
+ ) || []);
1174
+
1175
+ const removedParams = [...snapshotParams].filter(p => !currentParams.has(p));
1176
+ const addedRequired = (current.parameters || [])
1177
+ .filter(p => typeof p === 'object' && p.required && !snapshotParams.has(p.name))
1178
+ .map(p => p.name);
1179
+
1180
+ if (removedParams.length > 0) {
1181
+ changes.push({
1182
+ id: `tool-params-${name}`,
1183
+ category: 'tool',
1184
+ type: 'parameters-removed',
1185
+ field: `tools.${name}.parameters`,
1186
+ change: `Parameters removed from '${name}': [${removedParams.join(', ')}]`,
1187
+ severity: 'medium',
1188
+ autoFixable: false,
1189
+ affectedAgents: snapshotTool.referencedBy,
1190
+ });
1191
+ }
1192
+
1193
+ if (addedRequired.length > 0) {
1194
+ changes.push({
1195
+ id: `tool-required-${name}`,
1196
+ category: 'tool',
1197
+ type: 'new-required-parameters',
1198
+ field: `tools.${name}.parameters`,
1199
+ change: `New required parameters in '${name}': [${addedRequired.join(', ')}]`,
1200
+ severity: 'medium',
1201
+ autoFixable: false,
1202
+ affectedAgents: snapshotTool.referencedBy,
1203
+ });
1204
+ }
1205
+ }
1206
+
1207
+ return changes;
1208
+ }
1209
+
1210
+ // ─────────────────────────────────────────────
1211
+ // Step 5: Behavioral defaults diff
1212
+ // ─────────────────────────────────────────────
1213
+ function diffBehavioralDefaults(snapshot, openclawSchema) {
1214
+ const changes = [];
1215
+ const behavioralFields = [
1216
+ 'agents.defaults.sandbox.mode',
1217
+ 'agents.defaults.heartbeat.interval',
1218
+ 'agents.defaults.subagents.maxDepth',
1219
+ 'agents.defaults.model',
1220
+ ];
1221
+
1222
+ for (const field of behavioralFields) {
1223
+ const snapshotField = snapshot.fields.find(f => f.path === field);
1224
+ if (!snapshotField) continue;
1225
+
1226
+ // Extract default value from OpenClaw schema
1227
+ const currentDefault = extractDefaultValue(openclawSchema, field);
1228
+ const snapshotDefault = snapshotField.defaultValue;
1229
+
1230
+ if (snapshotDefault !== undefined && currentDefault !== undefined &&
1231
+ snapshotDefault !== currentDefault) {
1232
+ const severity = field.includes('sandbox') ? 'high' : 'low';
1233
+ changes.push({
1234
+ id: `behavioral-default-${field.replace(/[^a-z0-9]/gi, '-')}`,
1235
+ category: 'behavioral',
1236
+ type: 'default-changed',
1237
+ field,
1238
+ change: `Default value changed from '${snapshotDefault}' to '${currentDefault}'`,
1239
+ severity,
1240
+ autoFixable: severity === 'low',
1241
+ fix: severity === 'low' ? {
1242
+ type: 'config-inject',
1243
+ target: 'openclaw.json',
1244
+ jsonPath: `$.${field}`,
1245
+ value: snapshotDefault, // Pin to old default explicitly
1246
+ } : null,
1247
+ });
1248
+ }
1249
+ }
1250
+
1251
+ return changes;
1252
+ }
1253
+
1254
+ function extractDefaultValue(schema, fieldPath) {
1255
+ // Navigate the schema structure to find default values
1256
+ const parts = fieldPath.split('.');
1257
+ let current = schema;
1258
+ for (const part of parts) {
1259
+ if (!current) return undefined;
1260
+ current = current[part] || current.properties?.[part] || current.definitions?.[part];
1261
+ }
1262
+ return current?.default;
1263
+ }
1264
+
1265
+ // ─────────────────────────────────────────────
1266
+ // Step 6: Skills API diff
1267
+ // ─────────────────────────────────────────────
1268
+ function diffSkillsAPI(openclawVersion) {
1269
+ const changes = [];
1270
+
1271
+ // Check if SKILL.md format version changed
1272
+ let skillFormat;
1273
+ try {
1274
+ skillFormat = load('node_modules/openclaw/dist/skill-format.json');
1275
+ } catch {
1276
+ // Try to detect from source
1277
+ const candidates = [
1278
+ 'node_modules/openclaw/src/skills/loader.ts',
1279
+ 'node_modules/openclaw/lib/skills.js',
1280
+ ];
1281
+ for (const c of candidates) {
1282
+ const full = resolve(ROOT, c);
1283
+ if (existsSync(full)) {
1284
+ const src = readFileSync(full, 'utf8');
1285
+ if (src.includes('SKILL.md')) {
1286
+ // Check for format version indicators
1287
+ const vMatch = src.match(/skill[_-]?format[_-]?version\s*[:=]\s*['"]?(\d+)/i);
1288
+ if (vMatch) {
1289
+ skillFormat = { version: parseInt(vMatch[1]) };
1290
+ }
1291
+ }
1292
+ break;
1293
+ }
1294
+ }
1295
+ }
1296
+
1297
+ // Compare skill discovery paths
1298
+ const knownSkillPaths = [
1299
+ 'node_modules/openclaw/skills',
1300
+ ];
1301
+
1302
+ try {
1303
+ const openclawPkg = load('node_modules/openclaw/package.json');
1304
+ const configuredPaths = openclawPkg.openclawSkillPaths || [];
1305
+ const newPaths = configuredPaths.filter(p => !knownSkillPaths.includes(p));
1306
+
1307
+ if (newPaths.length > 0) {
1308
+ changes.push({
1309
+ id: 'skills-new-discovery-path',
1310
+ category: 'skills',
1311
+ type: 'discovery-path-added',
1312
+ field: 'skills.discoveryPaths',
1313
+ change: `New skill discovery paths added: [${newPaths.join(', ')}]`,
1314
+ severity: 'low',
1315
+ autoFixable: false, // Non-breaking warning
1316
+ isWarning: true,
1317
+ });
1318
+ }
1319
+ } catch {
1320
+ // No skill path config found, skip
1321
+ }
1322
+
1323
+ return changes;
1324
+ }
1325
+
1326
+ // ─────────────────────────────────────────────
1327
+ // Step 7: Build compatibility report
1328
+ // ─────────────────────────────────────────────
1329
+ function buildReport(allChanges, openclawVersion, aliceVersion) {
1330
+ const breaking = allChanges.filter(c => !c.isWarning);
1331
+ const warnings = allChanges.filter(c => c.isWarning);
1332
+
1333
+ const autoFixable = breaking.filter(c => c.autoFixable);
1334
+ const manualReview = breaking.filter(c => !c.autoFixable);
1335
+
1336
+ // Compute patch version (bump patch number)
1337
+ const [major, minor, patch] = aliceVersion.split('.').map(Number);
1338
+ const patchVersion = autoFixable.length > 0 && manualReview.length === 0
1339
+ ? `${major}.${minor}.${patch + 1}`
1340
+ : null;
1341
+
1342
+ // Build patches array
1343
+ const patches = autoFixable
1344
+ .filter(c => c.fix)
1345
+ .map(c => ({
1346
+ changeId: c.id,
1347
+ patchType: 'json-transform',
1348
+ targetFile: c.fix.target,
1349
+ operations: buildPatchOperations(c.fix),
1350
+ }));
1351
+
1352
+ return {
1353
+ $schema: 'https://alice.robbiesrobotics.com/schema/compatibility-report/v1.json',
1354
+ openclawVersion,
1355
+ aliceVersion,
1356
+ reportGeneratedAt: new Date().toISOString(),
1357
+ compatible: breaking.length === 0,
1358
+ summary: {
1359
+ breakingChangesCount: breaking.length,
1360
+ warningsCount: warnings.length,
1361
+ autoFixableCount: autoFixable.length,
1362
+ manualReviewCount: manualReview.length,
1363
+ },
1364
+ breakingChanges: breaking.map(c => ({
1365
+ id: c.id,
1366
+ category: c.category,
1367
+ severity: c.severity,
1368
+ autoFixable: c.autoFixable,
1369
+ field: c.field,
1370
+ change: c.change,
1371
+ affectedAgents: c.affectedAgents || [],
1372
+ fix: c.fix || null,
1373
+ manualSteps: c.manualSteps || null,
1374
+ rollbackable: c.autoFixable,
1375
+ verificationCommand: c.verificationCommand || null,
1376
+ })),
1377
+ warnings: warnings.map(w => ({
1378
+ id: w.id,
1379
+ category: w.category,
1380
+ severity: w.severity || 'low',
1381
+ field: w.field,
1382
+ change: w.change,
1383
+ actionRequired: false,
1384
+ })),
1385
+ patches,
1386
+ alicePatchVersion: patchVersion,
1387
+ alicePatchNpmTag: patchVersion
1388
+ ? `@robbiesrobotics/alice-agents@${patchVersion}`
1389
+ : null,
1390
+ };
1391
+ }
1392
+
1393
+ function buildPatchOperations(fix) {
1394
+ switch (fix.type) {
1395
+ case 'value-rename':
1396
+ return [{ op: 'replace-value', jsonPath: fix.jsonPath, from: fix.from, to: fix.to }];
1397
+ case 'config-inject':
1398
+ return [{ op: 'set', jsonPath: fix.jsonPath, value: fix.value }];
1399
+ case 'field-remove':
1400
+ return [{ op: 'remove', jsonPath: fix.jsonPath }];
1401
+ default:
1402
+ return [];
1403
+ }
1404
+ }
1405
+
1406
+ // ─────────────────────────────────────────────
1407
+ // Step 8: Apply patches to package files
1408
+ // ─────────────────────────────────────────────
1409
+ function applyPatches(report) {
1410
+ console.log(`Applying ${report.patches.length} patches...`);
1411
+
1412
+ for (const patch of report.patches) {
1413
+ console.log(` Patching ${patch.targetFile} (${patch.changeId})`);
1414
+ applyJsonTransform(patch.targetFile, patch.operations);
1415
+ }
1416
+
1417
+ // Update the snap shots to reflect the new baseline
1418
+ console.log('Updating snapshots to reflect applied patches...');
1419
+ // Snapshot update happens separately via --update-snapshot flag
1420
+ }
1421
+
1422
+ function applyJsonTransform(targetFile, operations) {
1423
+ const filePath = resolve(ROOT, targetFile);
1424
+ if (!existsSync(filePath)) {
1425
+ console.warn(` Target file not found: ${targetFile}, skipping`);
1426
+ return;
1427
+ }
1428
+
1429
+ const data = JSON.parse(readFileSync(filePath, 'utf8'));
1430
+
1431
+ for (const op of operations) {
1432
+ applyOperation(data, op);
1433
+ }
1434
+
1435
+ writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf8');
1436
+ }
1437
+
1438
+ function applyOperation(obj, op) {
1439
+ // Simplified JSON path operations for patch application
1440
+ // In production, use a library like jsonpath-plus
1441
+ switch (op.op) {
1442
+ case 'set': {
1443
+ setByPath(obj, op.jsonPath, op.value);
1444
+ break;
1445
+ }
1446
+ case 'replace-value': {
1447
+ replaceValues(obj, op.from, op.to);
1448
+ break;
1449
+ }
1450
+ case 'remove': {
1451
+ removeByPath(obj, op.jsonPath);
1452
+ break;
1453
+ }
1454
+ }
1455
+ }
1456
+
1457
+ function setByPath(obj, path, value) {
1458
+ // Parse $. prefix and navigate
1459
+ const parts = path.replace(/^\$\./, '').split('.');
1460
+ const last = parts.pop();
1461
+ let current = obj;
1462
+ for (const part of parts) {
1463
+ if (!current[part]) current[part] = {};
1464
+ current = current[part];
1465
+ }
1466
+ current[last] = value;
1467
+ }
1468
+
1469
+ function replaceValues(obj, from, to) {
1470
+ if (typeof obj !== 'object' || obj === null) return;
1471
+ for (const key of Object.keys(obj)) {
1472
+ if (obj[key] === from) obj[key] = to;
1473
+ else if (typeof obj[key] === 'object') replaceValues(obj[key], from, to);
1474
+ }
1475
+ }
1476
+
1477
+ function removeByPath(obj, path) {
1478
+ const parts = path.replace(/^\$\./, '').split('.');
1479
+ const last = parts.pop();
1480
+ let current = obj;
1481
+ for (const part of parts) {
1482
+ if (!current[part]) return;
1483
+ current = current[part];
1484
+ }
1485
+ delete current[last];
1486
+ }
1487
+
1488
+ // ─────────────────────────────────────────────
1489
+ // Step 9: Generate GitHub issue body
1490
+ // ─────────────────────────────────────────────
1491
+ function generateIssueBody(report) {
1492
+ const lines = [
1493
+ `## 🚨 OpenClaw ${report.openclawVersion} — Breaking Changes Detected`,
1494
+ '',
1495
+ `**A.L.I.C.E. Version:** ${report.aliceVersion} `,
1496
+ `**Detected At:** ${report.reportGeneratedAt} `,
1497
+ `**Breaking Changes:** ${report.summary.breakingChangesCount} `,
1498
+ `**Auto-Fixable:** ${report.summary.autoFixableCount} `,
1499
+ `**Needs Manual Review:** ${report.summary.manualReviewCount} `,
1500
+ '',
1501
+ '---',
1502
+ '',
1503
+ '## Breaking Changes',
1504
+ '',
1505
+ ];
1506
+
1507
+ for (const change of report.breakingChanges) {
1508
+ lines.push(`### ${change.id}`);
1509
+ lines.push(`**Category:** ${change.category} | **Severity:** ${change.severity} | **Auto-Fixable:** ${change.autoFixable}`);
1510
+ lines.push('');
1511
+ lines.push(`**Field:** \`${change.field}\``);
1512
+ lines.push(`**Change:** ${change.change}`);
1513
+ if (change.affectedAgents?.length > 0) {
1514
+ lines.push(`**Affected Agents:** ${change.affectedAgents.join(', ')}`);
1515
+ }
1516
+ if (change.manualSteps) {
1517
+ lines.push('');
1518
+ lines.push('**Manual Steps Required:**');
1519
+ for (const step of change.manualSteps) {
1520
+ lines.push(`- ${step}`);
1521
+ }
1522
+ }
1523
+ lines.push('');
1524
+ }
1525
+
1526
+ lines.push('---');
1527
+ lines.push('');
1528
+ lines.push('## Next Steps');
1529
+ lines.push('');
1530
+ lines.push('1. Review each breaking change above');
1531
+ lines.push('2. Update affected agent configs manually');
1532
+ lines.push('3. Update `snapshots/schema-snapshot.json` to reflect new baseline');
1533
+ lines.push('4. Bump A.L.I.C.E. version and publish');
1534
+ lines.push('5. Close this issue');
1535
+
1536
+ return lines.join('\n');
1537
+ }
1538
+
1539
+ // ─────────────────────────────────────────────
1540
+ // Main
1541
+ // ─────────────────────────────────────────────
1542
+ async function main() {
1543
+ if (MODE_APPLY_PATCHES) {
1544
+ const report = load(reportPath);
1545
+ applyPatches(report);
1546
+ console.log('✅ Patches applied successfully');
1547
+ return;
1548
+ }
1549
+
1550
+ if (MODE_GENERATE_ISSUE) {
1551
+ const report = load(reportPath);
1552
+ console.log(generateIssueBody(report));
1553
+ return;
1554
+ }
1555
+
1556
+ if (MODE_UPDATE_SNAPSHOT) {
1557
+ console.log(`Updating snapshots for OpenClaw ${openclawVersion}...`);
1558
+ const { configSchema, toolRegistry } = extractOpenclawSchema();
1559
+ // Merge new fields into existing snapshot
1560
+ const existingSchema = load(`${snapshotsDir}schema-snapshot.json`);
1561
+ existingSchema.openclawVersion = openclawVersion;
1562
+ existingSchema.capturedAt = new Date().toISOString();
1563
+ existingSchema.checksum = sha256(configSchema);
1564
+ save(`${snapshotsDir}schema-snapshot.json`, existingSchema);
1565
+ console.log('✅ Snapshots updated');
1566
+ return;
1567
+ }
1568
+
1569
+ // Main check flow
1570
+ console.log(`🔍 Checking compatibility: A.L.I.C.E. ${aliceVersion} vs OpenClaw ${openclawVersion}`);
1571
+
1572
+ const snapshots = loadSnapshots(snapshotsDir);
1573
+ const { configSchema, toolRegistry } = extractOpenclawSchema();
1574
+
1575
+ const allChanges = [
1576
+ ...diffConfigSchema(snapshots.schema, configSchema),
1577
+ ...diffToolRegistry(snapshots.tools, toolRegistry),
1578
+ ...diffBehavioralDefaults(snapshots.schema, configSchema),
1579
+ ...diffSkillsAPI(openclawVersion),
1580
+ ];
1581
+
1582
+ const report = buildReport(allChanges, openclawVersion, aliceVersion);
1583
+
1584
+ if (outputPath) {
1585
+ save(outputPath, report);
1586
+ console.log(`✅ Report written to ${outputPath}`);
1587
+ } else {
1588
+ console.log(JSON.stringify(report, null, 2));
1589
+ }
1590
+
1591
+ console.log(`\nSummary: ${report.compatible ? '✅ Compatible' : '❌ Breaking changes found'}`);
1592
+ console.log(` Breaking: ${report.summary.breakingChangesCount}`);
1593
+ console.log(` Auto-fixable: ${report.summary.autoFixableCount}`);
1594
+ console.log(` Manual review: ${report.summary.manualReviewCount}`);
1595
+
1596
+ process.exit(report.compatible ? 0 : 1);
1597
+ }
1598
+
1599
+ main().catch(err => {
1600
+ console.error('❌ Compatibility checker failed:', err.message);
1601
+ process.exit(2);
1602
+ });
1603
+ ```
1604
+
1605
+ ---
1606
+
1607
+ ## 8. Local Remediation Cron
1608
+
1609
+ ### 8.1 OpenClaw Cron Configuration
1610
+
1611
+ The self-healing check is registered as an OpenClaw cron job in `openclaw.json`:
1612
+
1613
+ ```json
1614
+ {
1615
+ "cron": {
1616
+ "entries": [
1617
+ {
1618
+ "id": "alice-self-heal",
1619
+ "label": "A.L.I.C.E. Self-Healing Check",
1620
+ "schedule": "0 3 * * *",
1621
+ "command": "node ~/.openclaw/workspace-olivia/alice-agents/tools/local-remediation.mjs",
1622
+ "runOnStart": true,
1623
+ "onGatewayRestart": true,
1624
+ "timeout": 120,
1625
+ "retries": 1,
1626
+ "notifyOnFailure": true
1627
+ }
1628
+ ]
1629
+ }
1630
+ }
1631
+ ```
1632
+
1633
+ **Key options:**
1634
+ - `schedule: "0 3 * * *"` — runs at 3 AM daily (low-traffic window)
1635
+ - `runOnStart: true` — runs once when the gateway starts
1636
+ - `onGatewayRestart: true` — triggers after every gateway restart (catches update events)
1637
+ - `timeout: 120` — 2 minute max (should complete in <30s normally)
1638
+ - `notifyOnFailure: true` — alerts user if the remediation script itself crashes
1639
+
1640
+ ### 8.2 Alice Manifest Format
1641
+
1642
+ **File:** `.alice-manifest.json` (in the alice-agents package directory)
1643
+
1644
+ ```json
1645
+ {
1646
+ "$schema": "https://alice.robbiesrobotics.com/schema/manifest/v1.json",
1647
+ "aliceVersion": "1.1.1",
1648
+ "installedAt": "2026-03-14T12:00:00Z",
1649
+ "installedAgainstOpenclawVersion": "2026.3.14",
1650
+ "lastCompatibilityCheck": "2026-03-15T03:00:00Z",
1651
+ "lastCompatibilityCheckResult": "compatible",
1652
+ "pendingChanges": [],
1653
+ "appliedPatches": [
1654
+ {
1655
+ "patchId": "BC-2026.3.10-001",
1656
+ "appliedAt": "2026-03-11T03:02:14Z",
1657
+ "openclawVersion": "2026.3.10",
1658
+ "description": "Auto-renamed tool profile 'coding' to 'developer'"
1659
+ }
1660
+ ],
1661
+ "backups": [
1662
+ {
1663
+ "timestamp": "2026-03-11T03:02:10Z",
1664
+ "file": "openclaw.json",
1665
+ "backupPath": "openclaw.json.bak.selfheal-1741658530"
1666
+ }
1667
+ ]
1668
+ }
1669
+ ```
1670
+
1671
+ ### 8.3 Local Remediation Script
1672
+
1673
+ **File:** `tools/local-remediation.mjs`
1674
+
1675
+ ```javascript
1676
+ #!/usr/bin/env node
1677
+ // tools/local-remediation.mjs
1678
+ // A.L.I.C.E. Self-Healing System — Local Remediation
1679
+ // Runs as an OpenClaw cron job on the user's machine.
1680
+
1681
+ import { readFileSync, writeFileSync, existsSync, copyFileSync } from 'fs';
1682
+ import { execSync } from 'child_process';
1683
+ import { resolve, dirname } from 'path';
1684
+ import { fileURLToPath } from 'url';
1685
+
1686
+ const __dirname = dirname(fileURLToPath(import.meta.url));
1687
+ const ALICE_ROOT = resolve(__dirname, '..');
1688
+ const OPENCLAW_CONFIG = resolve(process.env.HOME, '.openclaw', 'openclaw.json');
1689
+ const MANIFEST_PATH = resolve(ALICE_ROOT, '.alice-manifest.json');
1690
+ const COMPAT_BASE_URL = 'https://raw.githubusercontent.com/robbiesrobotics-bot/alice/main/compatibility';
1691
+
1692
+ // ─────────────────────────────────────────────
1693
+ // Utilities
1694
+ // ─────────────────────────────────────────────
1695
+ function log(msg) { console.log(`[alice-selfheal] ${new Date().toISOString()} ${msg}`); }
1696
+ function warn(msg) { console.warn(`[alice-selfheal] ⚠️ ${msg}`); }
1697
+ function error(msg) { console.error(`[alice-selfheal] ❌ ${msg}`); }
1698
+
1699
+ function loadJson(path) {
1700
+ if (!existsSync(path)) return null;
1701
+ return JSON.parse(readFileSync(path, 'utf8'));
1702
+ }
1703
+
1704
+ function saveJson(path, data) {
1705
+ writeFileSync(path, JSON.stringify(data, null, 2) + '\n', 'utf8');
1706
+ }
1707
+
1708
+ function getCurrentOpenclawVersion() {
1709
+ try {
1710
+ const pkg = loadJson(resolve(process.env.HOME, '.local', 'share', 'fnm', 'node-versions',
1711
+ `v${process.version.slice(1)}`, 'installation', 'lib', 'node_modules', 'openclaw', 'package.json'));
1712
+ if (pkg) return pkg.version;
1713
+ // Fallback: run openclaw --version
1714
+ const out = execSync('openclaw --version 2>/dev/null', { encoding: 'utf8' }).trim();
1715
+ return out.replace(/^openclaw\s+/i, '');
1716
+ } catch {
1717
+ // Try npm
1718
+ try {
1719
+ return execSync('npm list -g openclaw --json 2>/dev/null', { encoding: 'utf8' });
1720
+ } catch {
1721
+ return null;
1722
+ }
1723
+ }
1724
+ }
1725
+
1726
+ async function fetchCompatibilityReport(openclawVersion) {
1727
+ const url = `${COMPAT_BASE_URL}/${openclawVersion}.json`;
1728
+ log(`Fetching compatibility report from ${url}`);
1729
+
1730
+ try {
1731
+ const response = await fetch(url);
1732
+ if (response.status === 404) {
1733
+ log(`No compatibility report found for OpenClaw ${openclawVersion} — assuming compatible`);
1734
+ return null;
1735
+ }
1736
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
1737
+ return await response.json();
1738
+ } catch (err) {
1739
+ warn(`Could not fetch compatibility report: ${err.message}`);
1740
+ return null;
1741
+ }
1742
+ }
1743
+
1744
+ // ─────────────────────────────────────────────
1745
+ // Backup
1746
+ // ─────────────────────────────────────────────
1747
+ function backupConfig(timestamp) {
1748
+ const backupPath = `${OPENCLAW_CONFIG}.bak.selfheal-${timestamp}`;
1749
+ copyFileSync(OPENCLAW_CONFIG, backupPath);
1750
+ log(`Backed up openclaw.json → ${backupPath}`);
1751
+ return backupPath;
1752
+ }
1753
+
1754
+ // ─────────────────────────────────────────────
1755
+ // Gateway health check
1756
+ // ─────────────────────────────────────────────
1757
+ async function checkGatewayHealth() {
1758
+ try {
1759
+ const out = execSync('openclaw gateway status --json 2>/dev/null', { encoding: 'utf8', timeout: 10000 });
1760
+ const status = JSON.parse(out);
1761
+ return status.running === true;
1762
+ } catch {
1763
+ // Gateway status command failed — try HTTP health endpoint
1764
+ try {
1765
+ const res = await fetch('http://localhost:3000/health', { signal: AbortSignal.timeout(5000) });
1766
+ return res.ok;
1767
+ } catch {
1768
+ return false;
1769
+ }
1770
+ }
1771
+ }
1772
+
1773
+ async function restartGateway() {
1774
+ log('Restarting OpenClaw gateway...');
1775
+ try {
1776
+ execSync('openclaw gateway restart', { timeout: 30000 });
1777
+ // Wait for it to come up
1778
+ await new Promise(r => setTimeout(r, 5000));
1779
+ return await checkGatewayHealth();
1780
+ } catch (err) {
1781
+ error(`Gateway restart failed: ${err.message}`);
1782
+ return false;
1783
+ }
1784
+ }
1785
+
1786
+ // ─────────────────────────────────────────────
1787
+ // Rollback
1788
+ // ─────────────────────────────────────────────
1789
+ async function rollback(backupPath, manifest) {
1790
+ warn('Rolling back openclaw.json to pre-patch state...');
1791
+ copyFileSync(backupPath, OPENCLAW_CONFIG);
1792
+
1793
+ // Remove the backup entry from manifest and mark rollback
1794
+ manifest.pendingChanges = [];
1795
+ manifest.lastCompatibilityCheckResult = 'rolled-back';
1796
+ saveJson(MANIFEST_PATH, manifest);
1797
+
1798
+ // Restart with restored config
1799
+ const healthy = await restartGateway();
1800
+ if (healthy) {
1801
+ log('✅ Rollback successful — gateway healthy with original config');
1802
+ return true;
1803
+ } else {
1804
+ error('Gateway unhealthy even after rollback — manual intervention required');
1805
+ return false;
1806
+ }
1807
+ }
1808
+
1809
+ // ─────────────────────────────────────────────
1810
+ // Auto-fix application
1811
+ // ─────────────────────────────────────────────
1812
+ function applyAutoFixes(report, manifest) {
1813
+ const config = loadJson(OPENCLAW_CONFIG);
1814
+ let changeCount = 0;
1815
+
1816
+ for (const patch of report.patches) {
1817
+ log(` Applying patch: ${patch.changeId}`);
1818
+ for (const op of patch.operations) {
1819
+ try {
1820
+ applyOperation(config, op);
1821
+ changeCount++;
1822
+ } catch (err) {
1823
+ warn(` Failed to apply operation ${op.op} on ${op.jsonPath}: ${err.message}`);
1824
+ }
1825
+ }
1826
+ }
1827
+
1828
+ saveJson(OPENCLAW_CONFIG, config);
1829
+
1830
+ // Record applied patches in manifest
1831
+ for (const patch of report.patches) {
1832
+ const change = report.breakingChanges.find(c => c.id === patch.changeId);
1833
+ manifest.appliedPatches.push({
1834
+ patchId: patch.changeId,
1835
+ appliedAt: new Date().toISOString(),
1836
+ openclawVersion: report.openclawVersion,
1837
+ description: change?.change || patch.changeId,
1838
+ });
1839
+ }
1840
+
1841
+ return changeCount;
1842
+ }
1843
+
1844
+ function applyOperation(obj, op) {
1845
+ switch (op.op) {
1846
+ case 'set': setByPath(obj, op.jsonPath, op.value); break;
1847
+ case 'replace-value': replaceValues(obj, op.from, op.to); break;
1848
+ case 'remove': removeByPath(obj, op.jsonPath); break;
1849
+ }
1850
+ }
1851
+
1852
+ function setByPath(obj, path, value) {
1853
+ const parts = path.replace(/^\$\./, '').split('.');
1854
+ const last = parts.pop();
1855
+ let cur = obj;
1856
+ for (const p of parts) {
1857
+ if (!cur[p]) cur[p] = {};
1858
+ cur = cur[p];
1859
+ }
1860
+ cur[last] = value;
1861
+ }
1862
+
1863
+ function replaceValues(obj, from, to) {
1864
+ if (typeof obj !== 'object' || obj === null) return;
1865
+ for (const key of Object.keys(obj)) {
1866
+ if (obj[key] === from) obj[key] = to;
1867
+ else if (typeof obj[key] === 'object') replaceValues(obj[key], from, to);
1868
+ }
1869
+ }
1870
+
1871
+ function removeByPath(obj, path) {
1872
+ const parts = path.replace(/^\$\./, '').split('.');
1873
+ const last = parts.pop();
1874
+ let cur = obj;
1875
+ for (const p of parts) {
1876
+ if (!cur[p]) return;
1877
+ cur = cur[p];
1878
+ }
1879
+ delete cur[last];
1880
+ }
1881
+
1882
+ // ─────────────────────────────────────────────
1883
+ // Notifications
1884
+ // ─────────────────────────────────────────────
1885
+ async function notify(message, level = 'info') {
1886
+ log(`NOTIFY [${level}]: ${message}`);
1887
+
1888
+ // Write to Mission Control state file
1889
+ const mcState = loadJson(resolve(ALICE_ROOT, '.mission-control-state.json')) || { alerts: [] };
1890
+ mcState.alerts.unshift({
1891
+ id: `selfheal-${Date.now()}`,
1892
+ level,
1893
+ message,
1894
+ timestamp: new Date().toISOString(),
1895
+ source: 'self-healing',
1896
+ read: false,
1897
+ });
1898
+ // Keep last 50 alerts
1899
+ mcState.alerts = mcState.alerts.slice(0, 50);
1900
+ saveJson(resolve(ALICE_ROOT, '.mission-control-state.json'), mcState);
1901
+
1902
+ // Send Telegram notification if configured
1903
+ try {
1904
+ const config = loadJson(OPENCLAW_CONFIG);
1905
+ const telegramChannel = config?.notifications?.channels?.find(c => c.type === 'telegram');
1906
+ if (telegramChannel?.chatId && telegramChannel?.botToken) {
1907
+ const emoji = level === 'error' ? '🚨' : level === 'warn' ? '⚠️' : '✅';
1908
+ await fetch(`https://api.telegram.org/bot${telegramChannel.botToken}/sendMessage`, {
1909
+ method: 'POST',
1910
+ headers: { 'Content-Type': 'application/json' },
1911
+ body: JSON.stringify({
1912
+ chat_id: telegramChannel.chatId,
1913
+ text: `${emoji} *A.L.I.C.E. Self-Healing*\n\n${message}`,
1914
+ parse_mode: 'Markdown',
1915
+ }),
1916
+ });
1917
+ }
1918
+ } catch (err) {
1919
+ warn(`Telegram notification failed: ${err.message}`);
1920
+ }
1921
+ }
1922
+
1923
+ // ─────────────────────────────────────────────
1924
+ // Escalation — surface to Mission Control
1925
+ // ─────────────────────────────────────────────
1926
+ function escalateToMissionControl(report, manualChanges) {
1927
+ const escalations = loadJson(resolve(ALICE_ROOT, '.selfheal-escalations.json')) || { pending: [] };
1928
+
1929
+ for (const change of manualChanges) {
1930
+ // Avoid duplicate escalations
1931
+ if (escalations.pending.find(e => e.changeId === change.id)) continue;
1932
+
1933
+ escalations.pending.push({
1934
+ changeId: change.id,
1935
+ openclawVersion: report.openclawVersion,
1936
+ category: change.category,
1937
+ severity: change.severity,
1938
+ field: change.field,
1939
+ change: change.change,
1940
+ manualSteps: change.manualSteps,
1941
+ githubIssueUrl: change.githubIssueUrl || null,
1942
+ escalatedAt: new Date().toISOString(),
1943
+ resolved: false,
1944
+ });
1945
+ }
1946
+
1947
+ saveJson(resolve(ALICE_ROOT, '.selfheal-escalations.json'), escalations);
1948
+ }
1949
+
1950
+ // ─────────────────────────────────────────────
1951
+ // Main
1952
+ // ─────────────────────────────────────────────
1953
+ async function main() {
1954
+ log('Starting A.L.I.C.E. self-healing check...');
1955
+
1956
+ // Step 1: Load manifest
1957
+ const manifest = loadJson(MANIFEST_PATH);
1958
+ if (!manifest) {
1959
+ warn('No .alice-manifest.json found — skipping self-heal (A.L.I.C.E. not installed?)');
1960
+ return;
1961
+ }
1962
+
1963
+ // Step 2: Get current OpenClaw version
1964
+ const currentOcVersion = getCurrentOpenclawVersion();
1965
+ if (!currentOcVersion) {
1966
+ warn('Could not determine current OpenClaw version — skipping');
1967
+ return;
1968
+ }
1969
+
1970
+ const installedAgainst = manifest.installedAgainstOpenclawVersion;
1971
+ log(`Installed against: ${installedAgainst} | Current: ${currentOcVersion}`);
1972
+
1973
+ // Step 3: Check if versions diverged
1974
+ if (installedAgainst === currentOcVersion) {
1975
+ log('✅ OpenClaw version unchanged — no action needed');
1976
+ manifest.lastCompatibilityCheck = new Date().toISOString();
1977
+ manifest.lastCompatibilityCheckResult = 'compatible';
1978
+ saveJson(MANIFEST_PATH, manifest);
1979
+ return;
1980
+ }
1981
+
1982
+ log(`Version diverged! Fetching compatibility report for ${currentOcVersion}...`);
1983
+
1984
+ // Step 4: Fetch compatibility report
1985
+ const report = await fetchCompatibilityReport(currentOcVersion);
1986
+ if (!report) {
1987
+ warn(`No compatibility data available for OpenClaw ${currentOcVersion}`);
1988
+ manifest.lastCompatibilityCheck = new Date().toISOString();
1989
+ manifest.lastCompatibilityCheckResult = 'unknown';
1990
+ saveJson(MANIFEST_PATH, manifest);
1991
+ await notify(
1992
+ `OpenClaw updated to ${currentOcVersion} but no compatibility report is available yet. Monitor for issues.`,
1993
+ 'warn'
1994
+ );
1995
+ return;
1996
+ }
1997
+
1998
+ if (report.compatible) {
1999
+ log(`✅ OpenClaw ${currentOcVersion} is compatible with A.L.I.C.E. ${manifest.aliceVersion}`);
2000
+ manifest.installedAgainstOpenclawVersion = currentOcVersion;
2001
+ manifest.lastCompatibilityCheck = new Date().toISOString();
2002
+ manifest.lastCompatibilityCheckResult = 'compatible';
2003
+ saveJson(MANIFEST_PATH, manifest);
2004
+ return;
2005
+ }
2006
+
2007
+ log(`Found ${report.summary.breakingChangesCount} breaking change(s)`);
2008
+
2009
+ // Step 5: Separate auto-fixable from manual
2010
+ const autoFixable = report.breakingChanges.filter(c => c.autoFixable);
2011
+ const manualChanges = report.breakingChanges.filter(c => !c.autoFixable);
2012
+
2013
+ let timestamp = Math.floor(Date.now() / 1000);
2014
+ let backupPath = null;
2015
+
2016
+ // Step 6: Apply auto-fixes
2017
+ if (autoFixable.length > 0 && report.patches.length > 0) {
2018
+ log(`Auto-fixing ${autoFixable.length} change(s)...`);
2019
+
2020
+ // Backup first
2021
+ backupPath = backupConfig(timestamp);
2022
+ manifest.backups = manifest.backups || [];
2023
+ manifest.backups.push({
2024
+ timestamp: new Date().toISOString(),
2025
+ file: 'openclaw.json',
2026
+ backupPath,
2027
+ });
2028
+
2029
+ // Apply patches
2030
+ const fixCount = applyAutoFixes(report, manifest);
2031
+ log(`Applied ${fixCount} patch operation(s)`);
2032
+
2033
+ // Restart gateway to pick up changes
2034
+ const healthy = await restartGateway();
2035
+
2036
+ if (!healthy) {
2037
+ error('Gateway unhealthy after auto-fix — initiating rollback');
2038
+ await rollback(backupPath, manifest);
2039
+ await notify(
2040
+ `⚠️ OpenClaw ${currentOcVersion}: auto-patch failed health check and was rolled back. Manual intervention required.`,
2041
+ 'error'
2042
+ );
2043
+ return;
2044
+ }
2045
+
2046
+ // Update manifest
2047
+ manifest.installedAgainstOpenclawVersion = currentOcVersion;
2048
+ manifest.lastCompatibilityCheck = new Date().toISOString();
2049
+ manifest.lastCompatibilityCheckResult = manualChanges.length === 0 ? 'auto-fixed' : 'partial';
2050
+
2051
+ await notify(
2052
+ `A.L.I.C.E. auto-patched ${fixCount} config change(s) for OpenClaw ${currentOcVersion}. Gateway restarted and healthy. ✅`,
2053
+ 'info'
2054
+ );
2055
+ }
2056
+
2057
+ // Step 7: Escalate manual changes
2058
+ if (manualChanges.length > 0) {
2059
+ log(`Escalating ${manualChanges.length} change(s) requiring manual review...`);
2060
+ escalateToMissionControl(report, manualChanges);
2061
+
2062
+ await notify(
2063
+ `OpenClaw ${currentOcVersion} has ${manualChanges.length} change(s) that need your review. ` +
2064
+ `Open Mission Control → Settings → Compatibility to review and approve.`,
2065
+ 'warn'
2066
+ );
2067
+ }
2068
+
2069
+ // Save updated manifest
2070
+ saveJson(MANIFEST_PATH, manifest);
2071
+ log('Self-healing check complete.');
2072
+ }
2073
+
2074
+ main().catch(err => {
2075
+ error(`Self-healing script crashed: ${err.message}`);
2076
+ error(err.stack);
2077
+ process.exit(1);
2078
+ });
2079
+ ```
2080
+
2081
+ ---
2082
+
2083
+ ## 9. Mission Control UI
2084
+
2085
+ The Mission Control dashboard is the user-facing surface for reviewing and approving high-risk compatibility changes.
2086
+
2087
+ ### 9.1 Dashboard Location
2088
+
2089
+ **Path:** Mission Control → Settings → Compatibility
2090
+ **Data source:** `.selfheal-escalations.json` + `.mission-control-state.json`
2091
+
2092
+ ### 9.2 Compatibility Tab Layout
2093
+
2094
+ ```
2095
+ ┌──────────────────────────────────────────────────────────────┐
2096
+ │ Mission Control A.L.I.C.E. v1.1.1 │
2097
+ ├──────────────────────────────────────────────────────────────┤
2098
+ │ Dashboard │ Agents │ Logs │ Settings ▼ │
2099
+ │ └─ Compatibility ← active │
2100
+ ├──────────────────────────────────────────────────────────────┤
2101
+ │ │
2102
+ │ 🔴 COMPATIBILITY ALERTS [Dismiss All] │
2103
+ │ ───────────────────────────────────────────────────────── │
2104
+ │ OpenClaw 2026.4.0 — 1 change requires your review │
2105
+ │ │
2106
+ │ ┌─────────────────────────────────────────────────────┐ │
2107
+ │ │ ⚠️ HIGH: Sandbox mode default changed │ │
2108
+ │ │ │ │
2109
+ │ │ Field: agents.defaults.sandbox.mode │ │
2110
+ │ │ │ │
2111
+ │ │ BEFORE: "standard" │ │
2112
+ │ │ AFTER: "strict" │ │
2113
+ │ │ │ │
2114
+ │ │ Impact: exec calls without security parameter will │ │
2115
+ │ │ be blocked for all agents. Affected: dylan, devon, │ │
2116
+ │ │ avery, atlas. │ │
2117
+ │ │ │ │
2118
+ │ │ Manual steps: │ │
2119
+ │ │ 1. Review exec usage in each affected agent │ │
2120
+ │ │ 2. Add security: 'full' to trusted agents │ │
2121
+ │ │ 3. Add security: 'allowlist' to limited agents │ │
2122
+ │ │ 4. Restart gateway after changes │ │
2123
+ │ │ │ │
2124
+ │ │ 🔗 GitHub Issue #142 │ │
2125
+ │ │ │ │
2126
+ │ │ [View Diff] [Mark Resolved] [Spawn Devon] │ │
2127
+ │ └─────────────────────────────────────────────────────┘ │
2128
+ │ │
2129
+ │ ───────────────────────────────────────────────────────── │
2130
+ │ COMPATIBILITY HISTORY │
2131
+ │ │
2132
+ │ ✅ 2026.3.14 Compatible 2026-03-14 │
2133
+ │ 🔧 2026.3.10 Auto-fixed (1 change) 2026-03-10 │
2134
+ │ ✅ 2026.3.5 Compatible 2026-03-05 │
2135
+ │ ✅ 2026.3.1 Compatible 2026-03-01 │
2136
+ │ │
2137
+ │ ───────────────────────────────────────────────────────── │
2138
+ │ INSTALLED PATCHES │
2139
+ │ │
2140
+ │ BC-2026.3.10-001 2026-03-10 Auto-renamed tool profile │
2141
+ │ 'coding' → 'developer' │
2142
+ │ │
2143
+ │ ───────────────────────────────────────────────────────── │
2144
+ │ BACKUPS │
2145
+ │ │
2146
+ │ 2026-03-10 03:02 openclaw.json.bak.selfheal-1741658530 │
2147
+ │ [Restore] [Del] │
2148
+ │ │
2149
+ └──────────────────────────────────────────────────────────────┘
2150
+ ```
2151
+
2152
+ ### 9.3 Alert Data Structure
2153
+
2154
+ The Mission Control UI reads from `.mission-control-state.json`:
2155
+
2156
+ ```json
2157
+ {
2158
+ "alerts": [
2159
+ {
2160
+ "id": "selfheal-1741234567",
2161
+ "level": "warn",
2162
+ "message": "OpenClaw 2026.4.0 has 1 change(s) that need your review. Open Mission Control → Settings → Compatibility to review and approve.",
2163
+ "timestamp": "2026-04-01T03:02:14Z",
2164
+ "source": "self-healing",
2165
+ "read": false
2166
+ }
2167
+ ]
2168
+ }
2169
+ ```
2170
+
2171
+ ### 9.4 Escalation Data Structure
2172
+
2173
+ `.selfheal-escalations.json`:
2174
+
2175
+ ```json
2176
+ {
2177
+ "pending": [
2178
+ {
2179
+ "changeId": "BC-2026.4.0-003",
2180
+ "openclawVersion": "2026.4.0",
2181
+ "category": "behavioral",
2182
+ "severity": "high",
2183
+ "field": "agents.defaults.sandbox.mode",
2184
+ "change": "Default sandbox mode changed from 'standard' to 'strict'",
2185
+ "manualSteps": [
2186
+ "Review each affected agent's exec usage in their SOUL.md and config",
2187
+ "For trusted agents (dylan, devon): add security: 'full' to their agent config",
2188
+ "For limited agents (avery, atlas): add security: 'allowlist' with explicit allowed commands",
2189
+ "Test each agent's exec capability after update",
2190
+ "Update schema-snapshot.json to reflect new sandbox.mode default"
2191
+ ],
2192
+ "githubIssueUrl": "https://github.com/robbiesrobotics-bot/alice/issues/142",
2193
+ "escalatedAt": "2026-04-01T03:02:14Z",
2194
+ "resolved": false
2195
+ }
2196
+ ]
2197
+ }
2198
+ ```
2199
+
2200
+ ### 9.5 Action Buttons
2201
+
2202
+ | Button | Action |
2203
+ |--------|--------|
2204
+ | **View Diff** | Shows side-by-side before/after of affected config sections |
2205
+ | **Mark Resolved** | Sets `resolved: true` in escalations file, removes alert |
2206
+ | **Spawn Devon** | Triggers `sessions_spawn` for Devon with the full change context and manual steps |
2207
+ | **Restore** (backup) | Copies backup file back to `openclaw.json`, restarts gateway |
2208
+ | **Delete** (backup) | Removes backup file after user confirms |
2209
+
2210
+ ---
2211
+
2212
+ ## 10. Patch Generation
2213
+
2214
+ ### 10.1 Patch Types
2215
+
2216
+ | Type | Use Case | Risk |
2217
+ |------|----------|------|
2218
+ | `value-rename` | Enum value renamed | Low |
2219
+ | `config-inject` | New config key added with explicit value | Low |
2220
+ | `field-remove` | Deprecated field removed from config | Low |
2221
+ | `field-rename` | Config key renamed | Medium |
2222
+ | `structural` | Object restructured, requires data migration | High (manual) |
2223
+
2224
+ ### 10.2 Patch Operation Format
2225
+
2226
+ All auto-fix patches use a normalized operation format:
2227
+
2228
+ ```json
2229
+ {
2230
+ "changeId": "BC-2026.4.0-001",
2231
+ "patchType": "json-transform",
2232
+ "targetFile": "openclaw.json",
2233
+ "operations": [
2234
+ {
2235
+ "op": "replace-value",
2236
+ "jsonPath": "$.agents.list[*].tools.profile",
2237
+ "from": "coding",
2238
+ "to": "developer",
2239
+ "comment": "Rename tool profile enum value"
2240
+ },
2241
+ {
2242
+ "op": "set",
2243
+ "jsonPath": "$.agents.defaults.toolDefaults.web_fetch.extractMode",
2244
+ "value": "markdown",
2245
+ "comment": "Pin extractMode to preserve previous default behavior"
2246
+ },
2247
+ {
2248
+ "op": "remove",
2249
+ "jsonPath": "$.agents.defaults.legacyField",
2250
+ "comment": "Remove field deprecated in 2026.4.0"
2251
+ }
2252
+ ]
2253
+ }
2254
+ ```
2255
+
2256
+ ### 10.3 Patch Application Rules
2257
+
2258
+ 1. **Patches are applied in order** — operations within a patch run sequentially
2259
+ 2. **Patches are idempotent** — applying the same patch twice must be safe (no-op on second run)
2260
+ 3. **Patches are atomic** — if any operation in a patch fails, the whole patch is skipped and logged
2261
+ 4. **Patches only modify `openclaw.json`** — they do not modify agent SOUL.md, PLAYBOOK.md, or other files (those require manual review)
2262
+
2263
+ ### 10.4 NPM Patch Version Publishing
2264
+
2265
+ When the compatibility checker generates a patch:
2266
+
2267
+ 1. Auto-fix operations are baked into the A.L.I.C.E. package defaults
2268
+ 2. The package version is bumped by one patch increment (e.g., 1.1.1 → 1.1.2)
2269
+ 3. Published to npm as `@robbiesrobotics/alice-agents@1.1.2`
2270
+ 4. Users running `npm update @robbiesrobotics/alice-agents` get the fix baked in
2271
+ 5. The local remediation script also applies patches directly (does not require npm update)
2272
+
2273
+ ### 10.5 Verification After Patch
2274
+
2275
+ After applying patches, the local remediation script runs a verification:
2276
+
2277
+ ```javascript
2278
+ async function verifyPatch() {
2279
+ // 1. Validate openclaw.json syntax
2280
+ try {
2281
+ JSON.parse(readFileSync(OPENCLAW_CONFIG, 'utf8'));
2282
+ } catch {
2283
+ return { valid: false, reason: 'Invalid JSON after patch' };
2284
+ }
2285
+
2286
+ // 2. Run openclaw config validation if available
2287
+ try {
2288
+ execSync('openclaw validate --config ' + OPENCLAW_CONFIG, { timeout: 15000 });
2289
+ } catch (err) {
2290
+ return { valid: false, reason: `openclaw validate failed: ${err.message}` };
2291
+ }
2292
+
2293
+ // 3. Check gateway health after restart
2294
+ const healthy = await checkGatewayHealth();
2295
+ if (!healthy) return { valid: false, reason: 'Gateway unhealthy after restart' };
2296
+
2297
+ return { valid: true };
2298
+ }
2299
+ ```
2300
+
2301
+ ---
2302
+
2303
+ ## 11. Rollback Mechanism
2304
+
2305
+ ### 11.1 Backup Flow
2306
+
2307
+ Before any auto-fix is applied:
2308
+
2309
+ ```
2310
+ 1. Generate timestamp: Math.floor(Date.now() / 1000)
2311
+ 2. Copy openclaw.json → openclaw.json.bak.selfheal-{timestamp}
2312
+ 3. Record backup in .alice-manifest.json backups array
2313
+ 4. Proceed with patch application
2314
+ ```
2315
+
2316
+ **Backup file naming:** `openclaw.json.bak.selfheal-1741658530`
2317
+ (Unix timestamp ensures unique, sortable, human-readable names)
2318
+
2319
+ **Backup retention:** Backups are kept indefinitely until the user manually deletes them via Mission Control. Each backup is ~10-50KB. Purge policy: if more than 10 backups exist, the local remediation script will warn the user via Mission Control but will not auto-delete.
2320
+
2321
+ ### 11.2 Rollback Trigger Conditions
2322
+
2323
+ Rollback is triggered automatically if:
2324
+
2325
+ 1. `openclaw validate` exits with non-zero after patch
2326
+ 2. Gateway fails to restart after patch (timeout: 30s, health check fails)
2327
+ 3. Gateway health check fails within 60 seconds of restart
2328
+
2329
+ Rollback is **not** triggered for:
2330
+ - Agent-level errors (those are not gateway-health indicators)
2331
+ - Slow startup (extends timeout to 60s before failing)
2332
+
2333
+ ### 11.3 Rollback Flow
2334
+
2335
+ ```
2336
+ 1. Log: "Gateway unhealthy after auto-fix — initiating rollback"
2337
+ 2. Copy openclaw.json.bak.selfheal-{timestamp} → openclaw.json
2338
+ 3. Clear manifest.pendingChanges
2339
+ 4. Set manifest.lastCompatibilityCheckResult = 'rolled-back'
2340
+ 5. Save manifest
2341
+ 6. Run: openclaw gateway restart
2342
+ 7. Wait 5s
2343
+ 8. Check gateway health
2344
+ 9. If healthy:
2345
+ - Log: "Rollback successful"
2346
+ - Notify user: "Auto-patch rolled back. Review escalation in Mission Control."
2347
+ - Escalate all attempted changes to Mission Control as manual-review items
2348
+ 10. If not healthy:
2349
+ - Log: "Gateway unhealthy even after rollback — manual intervention required"
2350
+ - Notify user: "CRITICAL: Gateway unhealthy. Manual intervention required."
2351
+ - Do NOT attempt further automatic changes
2352
+ ```
2353
+
2354
+ ### 11.4 Manual Rollback via Mission Control
2355
+
2356
+ Users can also trigger rollback manually from Mission Control → Settings → Compatibility → Backups:
2357
+
2358
+ ```
2359
+ [Restore] button:
2360
+ 1. Confirm dialog: "Restore openclaw.json from backup {timestamp}? This will restart the gateway."
2361
+ 2. On confirm: run local-remediation.mjs --restore {backupPath}
2362
+ 3. Show result inline
2363
+ ```
2364
+
2365
+ ---
2366
+
2367
+ ## 12. Notification System
2368
+
2369
+ ### 12.1 Notification Channels
2370
+
2371
+ | Channel | Trigger | Content |
2372
+ |---------|---------|---------|
2373
+ | **Telegram** | Any self-heal event | Short summary message |
2374
+ | **Mission Control** | Any self-heal event | Full alert with action buttons |
2375
+ | **CLI stdout** | Cron job execution | Timestamped log lines |
2376
+
2377
+ ### 12.2 Telegram Message Format
2378
+
2379
+ **Auto-fixed (success):**
2380
+ ```
2381
+ ✅ A.L.I.C.E. Self-Healing
2382
+
2383
+ Auto-patched 2 config change(s) for OpenClaw 2026.4.0.
2384
+ Gateway restarted and healthy.
2385
+
2386
+ Changes applied:
2387
+ • Renamed tool profile 'coding' → 'developer'
2388
+ • Pinned web_fetch extractMode to 'markdown'
2389
+ ```
2390
+
2391
+ **Needs manual review:**
2392
+ ```
2393
+ ⚠️ A.L.I.C.E. Self-Healing
2394
+
2395
+ OpenClaw 2026.4.0 has 1 change(s) requiring your review.
2396
+
2397
+ Open Mission Control → Settings → Compatibility
2398
+ or view GitHub Issue #142
2399
+ ```
2400
+
2401
+ **Rollback occurred:**
2402
+ ```
2403
+ 🚨 A.L.I.C.E. Self-Healing — Rollback
2404
+
2405
+ Auto-patch for OpenClaw 2026.4.0 failed health check
2406
+ and was rolled back automatically.
2407
+
2408
+ Your config is restored to the previous working state.
2409
+ Manual intervention required.
2410
+
2411
+ Open Mission Control → Settings → Compatibility
2412
+ ```
2413
+
2414
+ **No changes needed:**
2415
+ *(No notification sent — silent success)*
2416
+
2417
+ ### 12.3 Notification Configuration
2418
+
2419
+ Telegram credentials are read from `openclaw.json`:
2420
+
2421
+ ```json
2422
+ {
2423
+ "notifications": {
2424
+ "channels": [
2425
+ {
2426
+ "id": "primary-telegram",
2427
+ "type": "telegram",
2428
+ "botToken": "{{env.TELEGRAM_BOT_TOKEN}}",
2429
+ "chatId": "{{env.TELEGRAM_CHAT_ID}}",
2430
+ "enabled": true,
2431
+ "events": ["self-healing"]
2432
+ }
2433
+ ]
2434
+ }
2435
+ }
2436
+ ```
2437
+
2438
+ ### 12.4 Notification Rate Limiting
2439
+
2440
+ To prevent spam during repeated failures:
2441
+
2442
+ - Max 1 notification per event type per OpenClaw version
2443
+ - If an escalation for a given `changeId` already exists in `.selfheal-escalations.json`, no repeat notification is sent
2444
+ - Rollback notifications are always sent regardless of rate limits
2445
+
2446
+ ---
2447
+
2448
+ ## 13. Error States & Edge Cases
2449
+
2450
+ | Scenario | Behavior |
2451
+ |----------|----------|
2452
+ | `openclaw.json` not found | Skip remediation, log warning |
2453
+ | `.alice-manifest.json` not found | Skip remediation (A.L.I.C.E. not installed) |
2454
+ | Compatibility report HTTP 404 | Assume compatible, log info, set `lastCompatibilityCheckResult: 'unknown'` |
2455
+ | Compatibility report HTTP 5xx | Retry once after 5s, then skip with warning |
2456
+ | Patch application crashes mid-way | Rollback from backup, escalate all changes as manual |
2457
+ | Gateway does not restart within 60s | Rollback, send critical notification |
2458
+ | Backup file missing during rollback | Log error, attempt gateway restart with current config anyway |
2459
+ | Duplicate escalation (same changeId) | Skip, do not create duplicate in `.selfheal-escalations.json` |
2460
+ | Self-heal cron fails to start | OpenClaw fires `notifyOnFailure: true` — user gets system-level alert |
2461
+ | OpenClaw version string unparseable | Skip version comparison, log warning |
2462
+ | Multiple OpenClaw updates in one day | Process only the most recent version; do not chain multiple reports |
2463
+
2464
+ ---
2465
+
2466
+ ## 14. File Layout Reference
2467
+
2468
+ ```
2469
+ alice-agents/
2470
+ ├── .alice-manifest.json # Installation + patch history
2471
+ ├── .mission-control-state.json # Alerts for Mission Control UI
2472
+ ├── .selfheal-escalations.json # Manual review queue
2473
+
2474
+ ├── snapshots/
2475
+ │ ├── schema-snapshot.json # Config field snapshot (Layer 1 source of truth)
2476
+ │ └── tool-snapshot.json # Tool registry snapshot (Layer 1 source of truth)
2477
+
2478
+ ├── compatibility/
2479
+ │ ├── 2026.3.14.json # Report per OpenClaw version
2480
+ │ ├── 2026.4.0.json
2481
+ │ └── ...
2482
+
2483
+ ├── tools/
2484
+ │ ├── compatibility-checker.mjs # Layer 1: CI script
2485
+ │ └── local-remediation.mjs # Layer 2: cron script
2486
+
2487
+ └── .github/
2488
+ └── workflows/
2489
+ └── compatibility-check.yml # GitHub Action
2490
+ ```
2491
+
2492
+ **OpenClaw config (on user's machine):**
2493
+ ```
2494
+ ~/.openclaw/
2495
+ ├── openclaw.json # Live config
2496
+ ├── openclaw.json.bak.selfheal-{ts} # Auto-backup before each patch
2497
+ └── workspace-olivia/
2498
+ └── alice-agents/ # This repo, checked out locally
2499
+ ```
2500
+
2501
+ ---
2502
+
2503
+ *End of spec. All formats, workflows, and scripts above are production-ready starting points. Implementation should add proper error handling, logging levels, and integration tests before first deployment.*