@robbiesrobotics/alice-agents 1.4.4 → 1.4.6

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/README.md CHANGED
@@ -22,6 +22,7 @@ That's it. The installer detects your runtime (NemoClaw or OpenClaw) and sets ev
22
22
  - installs the `mission-control-bridge` plugin into your OpenClaw home
23
23
  - writes a portable local Mission Control config at `~/.openclaw/.alice-mission-control.json`
24
24
  - enables the bridge in `openclaw.json` so your runtime can forward live telemetry to Mission Control
25
+ - installs a bundled `coding-agent` skill that prefers Codex for OpenAI defaults and Claude Code for Anthropic defaults
25
26
 
26
27
  An orchestrator (A.L.I.C.E., also addressable as Alice or Olivia) backed by specialist agents across every domain:
27
28
 
@@ -79,11 +80,14 @@ When you install, the installer will **auto-detect your configured model** and l
79
80
  # Interactive install
80
81
  npx @robbiesrobotics/alice-agents
81
82
 
82
- # Non-interactive with defaults (detected model if available, otherwise Sonnet; Starter tier)
83
+ # Non-interactive with defaults (detected model if available, otherwise Sonnet; Starter tier unless --tier pro)
83
84
  npx @robbiesrobotics/alice-agents --yes
84
85
 
85
86
  # Non-interactive Pro install with Mission Control Cloud enabled
86
- npx @robbiesrobotics/alice-agents --cloud --cloud-token YOUR_TOKEN
87
+ npx @robbiesrobotics/alice-agents --yes --tier pro --license-key YOUR_KEY --cloud --cloud-token YOUR_TOKEN
88
+
89
+ # Force the coding tool preference for this install
90
+ npx @robbiesrobotics/alice-agents --yes --coding-tool codex
87
91
 
88
92
  # Show help
89
93
  npx @robbiesrobotics/alice-agents --help
@@ -100,7 +104,7 @@ npx @robbiesrobotics/alice-agents --help
100
104
  If you're a Pro user with the cloud add-on, the installer can configure your local runtime for Mission Control in the same pass.
101
105
 
102
106
  - Interactive install: choose `Pro`, validate your license, then enable the Mission Control Cloud add-on when prompted
103
- - Non-interactive install: pass `--cloud`
107
+ - Non-interactive install: pass `--tier pro --license-key YOUR_KEY --cloud`
104
108
  - Optional flags:
105
109
  - `--cloud-token <token>` — access or ingest token for authenticated telemetry
106
110
  - `--cloud-dashboard-url <url>` — defaults to `https://alice.av3.ai`
@@ -127,6 +131,18 @@ npx @robbiesrobotics/alice-agents --uninstall
127
131
 
128
132
  Removes A.L.I.C.E. agents from `openclaw.json` while preserving any non-ALICE agents. Creates a backup before making changes.
129
133
 
134
+ ## Maintainer Release Guard
135
+
136
+ Maintainer publishes are now gated.
137
+
138
+ - `npm publish` runs `prepublishOnly`
139
+ - that hook runs tests, syntax checks, and `npm run release:check`
140
+ - publish is blocked unless the package version has a matching git tag on `HEAD`
141
+ - publish is blocked unless `landing/content/changelog.md` contains the matching version entry
142
+ - publish is blocked unless `releases/vX.Y.Z.md` exists and is marked `Status: approved`
143
+
144
+ Use `releases/TEMPLATE.md` as the starting point for each release brief.
145
+
130
146
  ## How It Works
131
147
 
132
148
  1. **You talk to A.L.I.C.E.** — she's your single point of contact
@@ -37,15 +37,22 @@ if (flags.has('--help') || flags.has('-h')) {
37
37
  npx @robbiesrobotics/alice-agents --help Show this help
38
38
 
39
39
  Options:
40
- --yes Skip prompts, use defaults (Sonnet, Starter tier)
40
+ --yes Skip prompts, use detected model when available (otherwise Sonnet)
41
41
  --update Non-interactive upgrade (alias for --yes with upgrade mode)
42
42
  --uninstall Remove A.L.I.C.E. agents (preserves non-ALICE agents)
43
43
  --doctor Run diagnostics and check install health
44
44
  --cloud Enable Mission Control Cloud setup during install
45
45
  --no-cloud Skip Mission Control Cloud setup during install
46
+ --tier <starter|pro> Force the install tier
47
+ --license-key <key> Provide a Pro license key for automation
48
+ --coding-tool <auto|claude|codex> Override the preferred coding CLI
46
49
  --cloud-token <token> Mission Control ingest/access token
47
50
  --cloud-dashboard-url <url> Mission Control dashboard URL
48
51
  --cloud-ingest-url <url> Mission Control ingest endpoint
52
+ --cloud-team-id <id> Mission Control team UUID for hosted linkage
53
+ --cloud-team-slug <slug> Mission Control team slug
54
+ --cloud-team-name <name> Mission Control team display name
55
+ --cloud-team-plan <plan> Mission Control team plan
49
56
  --version Print package version
50
57
  `);
51
58
  process.exit(0);
@@ -61,9 +68,16 @@ if (flags.has('--doctor')) {
61
68
  yes: true,
62
69
  modeOverride: 'upgrade',
63
70
  cloud: flags.has('--cloud') ? true : flags.has('--no-cloud') ? false : undefined,
71
+ tierOverride: getFlagValue('--tier'),
72
+ licenseKey: getFlagValue('--license-key'),
73
+ codingTool: getFlagValue('--coding-tool'),
64
74
  cloudToken: getFlagValue('--cloud-token'),
65
75
  cloudDashboardUrl: getFlagValue('--cloud-dashboard-url'),
66
76
  cloudIngestUrl: getFlagValue('--cloud-ingest-url'),
77
+ cloudTeamId: getFlagValue('--cloud-team-id'),
78
+ cloudTeamSlug: getFlagValue('--cloud-team-slug'),
79
+ cloudTeamName: getFlagValue('--cloud-team-name'),
80
+ cloudTeamPlan: getFlagValue('--cloud-team-plan'),
67
81
  }).catch((err) => {
68
82
  console.error(' ❌ Update failed:', err.message);
69
83
  process.exit(1);
@@ -82,9 +96,16 @@ if (flags.has('--doctor')) {
82
96
  runInstall({
83
97
  yes: flags.has('--yes'),
84
98
  cloud: flags.has('--cloud') ? true : flags.has('--no-cloud') ? false : undefined,
99
+ tierOverride: getFlagValue('--tier'),
100
+ licenseKey: getFlagValue('--license-key'),
101
+ codingTool: getFlagValue('--coding-tool'),
85
102
  cloudToken: getFlagValue('--cloud-token'),
86
103
  cloudDashboardUrl: getFlagValue('--cloud-dashboard-url'),
87
104
  cloudIngestUrl: getFlagValue('--cloud-ingest-url'),
105
+ cloudTeamId: getFlagValue('--cloud-team-id'),
106
+ cloudTeamSlug: getFlagValue('--cloud-team-slug'),
107
+ cloudTeamName: getFlagValue('--cloud-team-name'),
108
+ cloudTeamPlan: getFlagValue('--cloud-team-plan'),
88
109
  }).catch((err) => {
89
110
  console.error(' ❌ Install failed:', err.message);
90
111
  process.exit(1);
@@ -0,0 +1,33 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ const TEMPLATES_DIR = join(__dirname, '..', 'templates');
7
+
8
+ const STARTER_TEMPLATE = 'agents-starter.json';
9
+ const PRO_ADDONS_TEMPLATE = 'agents-pro.json';
10
+
11
+ export function normalizeTier(tier) {
12
+ return tier === 'pro' ? 'pro' : 'starter';
13
+ }
14
+
15
+ function loadRegistryFile(filename) {
16
+ const raw = readFileSync(join(TEMPLATES_DIR, filename), 'utf8');
17
+ return JSON.parse(raw);
18
+ }
19
+
20
+ export function loadAgentRegistry(tier = 'starter') {
21
+ const normalizedTier = normalizeTier(tier);
22
+ const starterAgents = loadRegistryFile(STARTER_TEMPLATE);
23
+ if (normalizedTier === 'starter') {
24
+ return starterAgents;
25
+ }
26
+
27
+ const proAddons = loadRegistryFile(PRO_ADDONS_TEMPLATE);
28
+ return [...starterAgents, ...proAddons];
29
+ }
30
+
31
+ export function getAgentIdsForTier(tier = 'starter') {
32
+ return loadAgentRegistry(tier).map((agent) => agent.id);
33
+ }
@@ -0,0 +1,187 @@
1
+ import { execSync } from 'node:child_process';
2
+
3
+ export const CODING_TOOL_OVERRIDES = new Set(['auto', 'claude', 'codex']);
4
+
5
+ const TOOL_CONFIG = {
6
+ claude: {
7
+ id: 'claude',
8
+ name: 'Claude Code',
9
+ cli: 'claude',
10
+ skillHeading: 'Claude Code',
11
+ primaryExample: `claude --permission-mode bypassPermissions --print 'YOUR TASK HERE. Run tests to verify.'`,
12
+ reviewExample: `claude --permission-mode bypassPermissions --print 'Review the current changes for bugs, regressions, and missing tests. Output a numbered list of findings.'`,
13
+ },
14
+ codex: {
15
+ id: 'codex',
16
+ name: 'Codex',
17
+ cli: 'codex',
18
+ skillHeading: 'Codex',
19
+ primaryExample: `codex exec --full-auto -C /path/to/project 'YOUR TASK HERE. Run tests to verify.'`,
20
+ reviewExample: `codex review --base main 'Review the current changes for bugs, regressions, and missing tests.'`,
21
+ },
22
+ };
23
+
24
+ function commandExists(cmd) {
25
+ const probe = process.platform === 'win32' ? 'where' : 'which';
26
+ try {
27
+ execSync(`${probe} ${cmd}`, { stdio: 'pipe' });
28
+ return true;
29
+ } catch {
30
+ return false;
31
+ }
32
+ }
33
+
34
+ export function normalizeProviderId(provider) {
35
+ if (!provider) return null;
36
+ if (provider === 'openai-codex') return 'openai';
37
+ return provider;
38
+ }
39
+
40
+ export function getModelProvider(model) {
41
+ if (!model || typeof model !== 'string' || !model.includes('/')) return null;
42
+ return normalizeProviderId(model.split('/')[0]);
43
+ }
44
+
45
+ function getPreferredToolForProvider(provider) {
46
+ if (provider === 'anthropic') return 'claude';
47
+ if (provider === 'openai') return 'codex';
48
+ return null;
49
+ }
50
+
51
+ function validateOverride(override) {
52
+ const normalized = String(override || 'auto').trim().toLowerCase() || 'auto';
53
+ if (!CODING_TOOL_OVERRIDES.has(normalized)) {
54
+ throw new Error(`Invalid --coding-tool value "${override}". Use auto, claude, or codex.`);
55
+ }
56
+ return normalized;
57
+ }
58
+
59
+ export function resolveCodingAgentPreference({ detectedModels = null, override = 'auto' } = {}) {
60
+ const normalizedOverride = validateOverride(override);
61
+ const available = {
62
+ claude: commandExists(TOOL_CONFIG.claude.cli),
63
+ codex: commandExists(TOOL_CONFIG.codex.cli),
64
+ };
65
+ const provider =
66
+ getModelProvider(detectedModels?.primary) ||
67
+ getModelProvider(detectedModels?.orchestrator) ||
68
+ (detectedModels?.providers || [])
69
+ .map(normalizeProviderId)
70
+ .find((entry) => entry === 'anthropic' || entry === 'openai') ||
71
+ null;
72
+
73
+ let preferredTool = normalizedOverride === 'auto' ? getPreferredToolForProvider(provider) : normalizedOverride;
74
+
75
+ if (!preferredTool) {
76
+ if (available.codex) {
77
+ preferredTool = 'codex';
78
+ } else if (available.claude) {
79
+ preferredTool = 'claude';
80
+ } else {
81
+ preferredTool = 'codex';
82
+ }
83
+ }
84
+
85
+ const fallbackTool = preferredTool === 'codex' ? 'claude' : 'codex';
86
+ const selectionReason =
87
+ normalizedOverride !== 'auto'
88
+ ? `manual override (--coding-tool ${preferredTool})`
89
+ : provider
90
+ ? `detected ${provider} as the default OpenClaw provider`
91
+ : available.codex || available.claude
92
+ ? 'no provider-specific default detected; using the first available coding CLI'
93
+ : 'no coding CLI detected yet; generated guidance includes both Codex and Claude Code';
94
+
95
+ return {
96
+ override: normalizedOverride,
97
+ provider,
98
+ preferredTool,
99
+ fallbackTool,
100
+ preferred: TOOL_CONFIG[preferredTool],
101
+ fallback: TOOL_CONFIG[fallbackTool],
102
+ available,
103
+ selectionReason,
104
+ skillId: 'coding-agent',
105
+ skillPath: '~/.openclaw/skills/coding-agent/SKILL.md',
106
+ };
107
+ }
108
+
109
+ export function buildCodingAgentSkillContent(preference) {
110
+ const preferredAvailability = preference.available[preference.preferred.id]
111
+ ? `${preference.preferred.cli} is installed on this machine.`
112
+ : `${preference.preferred.cli} is not currently installed; check before using it.`;
113
+ const fallbackAvailability = preference.available[preference.fallback.id]
114
+ ? `${preference.fallback.cli} is also available as a fallback.`
115
+ : `${preference.fallback.cli} is the fallback if you install it later.`;
116
+ const providerLabel = preference.provider || 'unknown';
117
+
118
+ return `---
119
+ name: coding-agent
120
+ description: 'Delegate substantial coding work to the preferred coding CLI for this install. Use when: multi-file implementation, refactors, build/test verification, deep codebase exploration, or structured code review. Prefer ${preference.preferred.name} first, then fall back to ${preference.fallback.name} if needed.'
121
+ metadata:
122
+ {
123
+ "openclaw": { "emoji": "⚙️", "requires": { "anyBins": ["${preference.preferred.cli}", "${preference.fallback.cli}"] } }
124
+ }
125
+ ---
126
+
127
+ # Coding Agent Skill
128
+
129
+ This bundled skill routes coding work through the preferred CLI for this install.
130
+
131
+ - Preferred tool: **${preference.preferred.name}**
132
+ - Fallback tool: **${preference.fallback.name}**
133
+ - Detection basis: ${preference.selectionReason}
134
+ - Current provider signal: ${providerLabel}
135
+
136
+ ## Availability
137
+
138
+ - ${preferredAvailability}
139
+ - ${fallbackAvailability}
140
+
141
+ ## When to use this skill
142
+
143
+ Use this skill when the task requires:
144
+ - Modifying multiple files
145
+ - Running build, lint, or test commands to verify correctness
146
+ - Exploring an unfamiliar codebase before implementing
147
+ - A focused code review with concrete findings
148
+
149
+ ## Preferred path: ${preference.preferred.skillHeading}
150
+
151
+ \`\`\`
152
+ ${preference.preferred.primaryExample}
153
+ \`\`\`
154
+
155
+ Review-only example:
156
+
157
+ \`\`\`
158
+ ${preference.preferred.reviewExample}
159
+ \`\`\`
160
+
161
+ ## Fallback path: ${preference.fallback.skillHeading}
162
+
163
+ \`\`\`
164
+ ${preference.fallback.primaryExample}
165
+ \`\`\`
166
+
167
+ Review-only example:
168
+
169
+ \`\`\`
170
+ ${preference.fallback.reviewExample}
171
+ \`\`\`
172
+
173
+ ## Rules
174
+
175
+ 1. Use the preferred tool first unless it is unavailable or blocked.
176
+ 2. Always point the tool at the project root before asking it to work.
177
+ 3. Ask it to run the relevant verification commands before finishing.
178
+ 4. Summarize what changed and any remaining risks when reporting back.
179
+ 5. Do not use this skill for trivial one-line edits or pure planning.
180
+
181
+ ## Quick check
182
+
183
+ \`\`\`
184
+ which ${preference.preferred.cli} || which ${preference.fallback.cli}
185
+ \`\`\`
186
+ `;
187
+ }
@@ -272,7 +272,11 @@ export function mergeConfig({ agents, mode, preset, customModels }) {
272
272
  config.tools = config.tools || {};
273
273
  config.tools.agentToAgent = config.tools.agentToAgent || {};
274
274
  config.tools.agentToAgent.enabled = true;
275
- config.tools.agentToAgent.allow = [...aliceIds];
275
+ const mergedAllow = new Set(config.tools.agentToAgent.allow || []);
276
+ for (const id of aliceIds) {
277
+ mergedAllow.add(id);
278
+ }
279
+ config.tools.agentToAgent.allow = [...mergedAllow];
276
280
 
277
281
  writeConfigAtomic(config);
278
282
  return { backupPath, agentCount: aliceEntries.length, effectivePreset, warning };
package/lib/doctor.mjs CHANGED
@@ -5,6 +5,7 @@ import { execSync } from 'node:child_process';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import { icons, greenBold, green, red, yellow, cyan, dim, bold,
7
7
  printSection, printSeparator, separator } from './colors.mjs';
8
+ import { getAgentIdsForTier } from './agent-registry.mjs';
8
9
 
9
10
  const __dirname = dirname(fileURLToPath(import.meta.url));
10
11
  const HOME = homedir();
@@ -20,10 +21,52 @@ function commandExists(cmd) {
20
21
  }
21
22
  const OPENCLAW_DIR = join(HOME, '.openclaw');
22
23
 
23
- const STARTER_AGENTS = [
24
- 'olivia', 'dylan', 'selena', 'devon', 'quinn',
25
- 'felix', 'daphne', 'rowan', 'darius', 'sophie',
26
- ];
24
+ const STARTER_AGENTS = getAgentIdsForTier('starter');
25
+ const ALL_ALICE_AGENTS = getAgentIdsForTier('pro');
26
+
27
+ function normalizeProviderId(provider) {
28
+ if (!provider) return null;
29
+ if (provider === 'openai-codex') return 'openai';
30
+ return provider;
31
+ }
32
+
33
+ function getConfigAgents(config) {
34
+ if (Array.isArray(config?.agents?.list)) return config.agents.list;
35
+ if (Array.isArray(config?.agents)) return config.agents;
36
+ return [];
37
+ }
38
+
39
+ function detectConfiguredModel(config) {
40
+ if (!config || config === 'invalid') return { ok: false, label: null, inherited: false };
41
+
42
+ const defaults = config?.agents?.defaults?.model || {};
43
+ const primary = defaults.primary || config?.model || config?.default_model || null;
44
+ if (primary) {
45
+ return { ok: true, label: primary, inherited: true };
46
+ }
47
+
48
+ const providerKeys = Object.keys(config?.models?.providers || {});
49
+ if (providerKeys.length > 0) {
50
+ return { ok: true, label: providerKeys[0], inherited: false };
51
+ }
52
+
53
+ const profile = Object.values(config?.auth?.profiles || {}).find((entry) => entry?.provider);
54
+ if (profile?.provider) {
55
+ return { ok: true, label: normalizeProviderId(profile.provider), inherited: false };
56
+ }
57
+
58
+ if (config.models && Object.keys(config.models).length > 0) {
59
+ return { ok: true, label: Object.keys(config.models)[0], inherited: false };
60
+ }
61
+ if (config.providers && Object.keys(config.providers).length > 0) {
62
+ return { ok: true, label: Object.keys(config.providers)[0], inherited: false };
63
+ }
64
+ if (config.llm && Object.keys(config.llm).length > 0) {
65
+ return { ok: true, label: Object.keys(config.llm)[0], inherited: false };
66
+ }
67
+
68
+ return { ok: false, label: null, inherited: false };
69
+ }
27
70
 
28
71
  function check(label, ok, hint) {
29
72
  const icon = ok ? icons.ok : icons.fail;
@@ -158,10 +201,23 @@ export async function runDoctor() {
158
201
  }
159
202
 
160
203
  // 3. A.L.I.C.E. agents in config
161
- const configAgents = Array.isArray(config.agents) ? config.agents : [];
204
+ const manifest = (() => {
205
+ try {
206
+ const mPath = join(OPENCLAW_DIR, '.alice-manifest.json');
207
+ if (!existsSync(mPath)) return null;
208
+ return JSON.parse(readFileSync(mPath, 'utf8'));
209
+ } catch {
210
+ return null;
211
+ }
212
+ })();
213
+ const configAgents = getConfigAgents(config);
162
214
  const agentsInConfig = configAgents
163
- .filter((a) => a && STARTER_AGENTS.includes(a.id))
215
+ .filter((a) => a && ALL_ALICE_AGENTS.includes(a.id))
164
216
  .map((a) => a.id);
217
+ const expectedTier = manifest?.tier === 'pro' || agentsInConfig.some((id) => !STARTER_AGENTS.includes(id))
218
+ ? 'pro'
219
+ : 'starter';
220
+ const expectedAgents = expectedTier === 'pro' ? ALL_ALICE_AGENTS : STARTER_AGENTS;
165
221
 
166
222
  const agentsOk = agentsInConfig.length > 0;
167
223
  check(
@@ -173,17 +229,17 @@ export async function runDoctor() {
173
229
  );
174
230
  allOk = allOk && agentsOk;
175
231
 
176
- // Check for missing agents from full starter set
177
- if (agentsInConfig.length > 0 && agentsInConfig.length < STARTER_AGENTS.length) {
178
- const missing = STARTER_AGENTS.filter((id) => !agentsInConfig.includes(id));
232
+ // Check for missing agents from the expected tier roster
233
+ if (agentsInConfig.length > 0 && agentsInConfig.length < expectedAgents.length) {
234
+ const missing = expectedAgents.filter((id) => !agentsInConfig.includes(id));
179
235
  check(
180
- `All starter agents present (missing: ${missing.join(', ')})`,
236
+ `All ${expectedTier} agents present (missing: ${missing.join(', ')})`,
181
237
  false,
182
238
  'Run: npx @robbiesrobotics/alice-agents --update'
183
239
  );
184
240
  allOk = false;
185
- } else if (agentsInConfig.length === STARTER_AGENTS.length) {
186
- check('All starter agents present', true);
241
+ } else if (agentsInConfig.length === expectedAgents.length) {
242
+ check(`All ${expectedTier} agents present`, true);
187
243
  }
188
244
 
189
245
  // 4. Agent workspaces exist on disk
@@ -207,26 +263,13 @@ export async function runDoctor() {
207
263
  }
208
264
 
209
265
  // 5. At least one model/provider configured
210
- let modelOk = false;
211
- let modelLabel = null;
212
-
213
- if (config.default_model) {
214
- modelOk = true;
215
- modelLabel = config.default_model;
216
- } else if (config.models && Object.keys(config.models).length > 0) {
217
- modelOk = true;
218
- modelLabel = Object.keys(config.models)[0];
219
- } else if (config.providers && Object.keys(config.providers).length > 0) {
220
- modelOk = true;
221
- modelLabel = Object.keys(config.providers)[0];
222
- } else if (config.llm && Object.keys(config.llm).length > 0) {
223
- modelOk = true;
224
- modelLabel = Object.keys(config.llm)[0];
225
- }
266
+ const modelState = detectConfiguredModel(config);
267
+ const modelOk = modelState.ok;
268
+ const modelLabel = modelState.label;
226
269
 
227
270
  check(
228
271
  modelOk
229
- ? `Model/provider configured: ${modelLabel}`
272
+ ? `Model/provider configured: ${modelLabel}${modelState.inherited ? ' (shared default)' : ''}`
230
273
  : 'No model/provider configured',
231
274
  modelOk,
232
275
  'Run: openclaw configure to set up a model provider'
@@ -246,9 +289,11 @@ export async function runDoctor() {
246
289
  );
247
290
  // Docker permission issue is a warning, not a hard failure for A.L.I.C.E. itself
248
291
  // but it will break OpenClaw's own Docker features
292
+ allOk = false;
249
293
  console.log(` ${icons.info} ${dim('This will affect OpenClaw features that use Docker.')}\n`);
250
294
  } else {
251
295
  check('Docker daemon not running or not accessible', false, docker.hint);
296
+ allOk = false;
252
297
  }
253
298
  }
254
299
 
@@ -260,24 +305,28 @@ export async function runDoctor() {
260
305
 
261
306
  // 7. License check
262
307
  const { checkProLicense } = await import('./license.mjs');
263
- const manifest = (() => {
264
- try {
265
- const mPath = join(OPENCLAW_DIR, '.alice-manifest.json');
266
- if (!existsSync(mPath)) return null;
267
- return JSON.parse(readFileSync(mPath, 'utf8'));
268
- } catch {
269
- return null;
270
- }
271
- })();
272
308
 
273
309
  const licenseResult = await checkProLicense();
274
310
  if (manifest?.tier === 'pro' || licenseResult.licensed) {
275
311
  if (licenseResult.licensed) {
276
312
  const maskedKey = licenseResult.key.slice(0, 13) + '****';
277
- check(
278
- `Pro license: ${maskedKey} (stored at ~/.alice/license)`,
279
- true
280
- );
313
+ if (licenseResult.provisional) {
314
+ check(
315
+ `Pro license: ${maskedKey} (temporary grace until ${licenseResult.graceUntil})`,
316
+ false,
317
+ 'Reconnect to the network and rerun the installer to complete validation'
318
+ );
319
+ allOk = false;
320
+ } else {
321
+ check(
322
+ `Pro license: ${maskedKey} (stored at ~/.alice/license)`,
323
+ true
324
+ );
325
+ if (licenseResult.needsRevalidation) {
326
+ console.log(` ${icons.info} ${dim('Validation service unavailable — using cached entitlement for now.')}`);
327
+ console.log('');
328
+ }
329
+ }
281
330
  } else {
282
331
  check(
283
332
  'Pro license: not found (running Starter tier)',
@@ -291,7 +340,38 @@ export async function runDoctor() {
291
340
  check('License: Starter tier (no license required)', true);
292
341
  }
293
342
 
294
- // 8. Skills disk check
343
+ // 8. Mission Control cloud config
344
+ const missionControlConfigPath = join(OPENCLAW_DIR, '.alice-mission-control.json');
345
+ if (existsSync(missionControlConfigPath)) {
346
+ try {
347
+ const missionControlConfig = JSON.parse(readFileSync(missionControlConfigPath, 'utf8'));
348
+ const cloud = missionControlConfig?.cloud || {};
349
+ const hasDashboardUrl = typeof cloud.dashboardUrl === 'string' && cloud.dashboardUrl.length > 0;
350
+ const hasIngestUrl = typeof cloud.ingestUrl === 'string' && cloud.ingestUrl.length > 0;
351
+ const hasIngestToken = typeof cloud.ingestToken === 'string' && cloud.ingestToken.length > 0;
352
+ const cloudOk = hasDashboardUrl && hasIngestUrl && hasIngestToken;
353
+
354
+ check(
355
+ cloudOk
356
+ ? `Mission Control cloud configured (${cloud.dashboardUrl})`
357
+ : 'Mission Control cloud config incomplete',
358
+ cloudOk,
359
+ 'Run: npx @robbiesrobotics/alice-agents --cloud to repair cloud settings'
360
+ );
361
+ allOk = allOk && cloudOk;
362
+ } catch {
363
+ check(
364
+ 'Mission Control cloud config invalid',
365
+ false,
366
+ 'Repair ~/.openclaw/.alice-mission-control.json or rerun the installer with --cloud'
367
+ );
368
+ allOk = false;
369
+ }
370
+ } else {
371
+ console.log(` ${dim('–')} ${dim('Mission Control cloud not configured (optional)')}`);
372
+ }
373
+
374
+ // 9. Skills disk check
295
375
  const skillsManifestPath = join(OPENCLAW_DIR, '.alice-manifest.json');
296
376
  const skillsManifestData = (() => {
297
377
  try {