@robbiesrobotics/alice-agents 1.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SELF-HEALING-SPEC.md +2503 -0
- package/lib/installer.mjs +17 -1
- package/package.json +4 -1
- package/snapshots/schema-snapshot.json +221 -0
- package/snapshots/tool-snapshot.json +37 -0
- package/tools/compatibility-checker.mjs +268 -0
- package/tools/local-remediation.mjs +231 -0
|
@@ -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.*
|