@robbiesrobotics/alice-agents 1.4.5 → 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,8 @@ 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');
27
26
 
28
27
  function normalizeProviderId(provider) {
29
28
  if (!provider) return null;
@@ -202,10 +201,23 @@ export async function runDoctor() {
202
201
  }
203
202
 
204
203
  // 3. A.L.I.C.E. agents in config
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
+ })();
205
213
  const configAgents = getConfigAgents(config);
206
214
  const agentsInConfig = configAgents
207
- .filter((a) => a && STARTER_AGENTS.includes(a.id))
215
+ .filter((a) => a && ALL_ALICE_AGENTS.includes(a.id))
208
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;
209
221
 
210
222
  const agentsOk = agentsInConfig.length > 0;
211
223
  check(
@@ -217,17 +229,17 @@ export async function runDoctor() {
217
229
  );
218
230
  allOk = allOk && agentsOk;
219
231
 
220
- // Check for missing agents from full starter set
221
- if (agentsInConfig.length > 0 && agentsInConfig.length < STARTER_AGENTS.length) {
222
- 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));
223
235
  check(
224
- `All starter agents present (missing: ${missing.join(', ')})`,
236
+ `All ${expectedTier} agents present (missing: ${missing.join(', ')})`,
225
237
  false,
226
238
  'Run: npx @robbiesrobotics/alice-agents --update'
227
239
  );
228
240
  allOk = false;
229
- } else if (agentsInConfig.length === STARTER_AGENTS.length) {
230
- check('All starter agents present', true);
241
+ } else if (agentsInConfig.length === expectedAgents.length) {
242
+ check(`All ${expectedTier} agents present`, true);
231
243
  }
232
244
 
233
245
  // 4. Agent workspaces exist on disk
@@ -277,9 +289,11 @@ export async function runDoctor() {
277
289
  );
278
290
  // Docker permission issue is a warning, not a hard failure for A.L.I.C.E. itself
279
291
  // but it will break OpenClaw's own Docker features
292
+ allOk = false;
280
293
  console.log(` ${icons.info} ${dim('This will affect OpenClaw features that use Docker.')}\n`);
281
294
  } else {
282
295
  check('Docker daemon not running or not accessible', false, docker.hint);
296
+ allOk = false;
283
297
  }
284
298
  }
285
299
 
@@ -291,24 +305,28 @@ export async function runDoctor() {
291
305
 
292
306
  // 7. License check
293
307
  const { checkProLicense } = await import('./license.mjs');
294
- const manifest = (() => {
295
- try {
296
- const mPath = join(OPENCLAW_DIR, '.alice-manifest.json');
297
- if (!existsSync(mPath)) return null;
298
- return JSON.parse(readFileSync(mPath, 'utf8'));
299
- } catch {
300
- return null;
301
- }
302
- })();
303
308
 
304
309
  const licenseResult = await checkProLicense();
305
310
  if (manifest?.tier === 'pro' || licenseResult.licensed) {
306
311
  if (licenseResult.licensed) {
307
312
  const maskedKey = licenseResult.key.slice(0, 13) + '****';
308
- check(
309
- `Pro license: ${maskedKey} (stored at ~/.alice/license)`,
310
- true
311
- );
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
+ }
312
330
  } else {
313
331
  check(
314
332
  'Pro license: not found (running Starter tier)',
package/lib/installer.mjs CHANGED
@@ -27,6 +27,8 @@ import { c, bold, dim, green, greenBold, red, yellow, cyan, gray,
27
27
  icons, separator, printSection, printSeparator, printBox,
28
28
  printStepDone, printStepFail, printStepSkip } from './colors.mjs';
29
29
  import { runSkillsWizardStep } from './skills.mjs';
30
+ import { resolveCodingAgentPreference } from './coding-agent.mjs';
31
+ import { loadAgentRegistry } from './agent-registry.mjs';
30
32
 
31
33
  function commandExists(cmd) {
32
34
  const probe = process.platform === 'win32' ? 'where' : 'which';
@@ -105,7 +107,11 @@ function getOpenClawVersion() {
105
107
 
106
108
  function getLatestNpmVersion() {
107
109
  try {
108
- const output = execSync('npm view openclaw version', { stdio: 'pipe', encoding: 'utf8' });
110
+ const output = execSync('npm view openclaw version', {
111
+ stdio: 'pipe',
112
+ encoding: 'utf8',
113
+ timeout: 5000,
114
+ });
109
115
  return output.trim();
110
116
  } catch {
111
117
  return null;
@@ -421,11 +427,6 @@ async function installRuntime(auto) {
421
427
 
422
428
  const __dirname = dirname(fileURLToPath(import.meta.url));
423
429
 
424
- function loadAgentRegistry() {
425
- const raw = readFileSync(join(__dirname, '..', 'templates', 'agents-starter.json'), 'utf8');
426
- return JSON.parse(raw);
427
- }
428
-
429
430
  function printBanner() {
430
431
  const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url)));
431
432
  const version = pkg.version || '';
@@ -446,6 +447,13 @@ function printSummary(mode, tier, agents, preset, userInfo, detectedModels) {
446
447
  return printSummaryWithOptions(mode, tier, agents, preset, userInfo, detectedModels, null);
447
448
  }
448
449
 
450
+ function normalizeTierOption(tier) {
451
+ if (!tier) return null;
452
+ const normalized = String(tier).trim().toLowerCase();
453
+ if (normalized === 'starter' || normalized === 'pro') return normalized;
454
+ throw new Error(`Invalid --tier value "${tier}". Use starter or pro.`);
455
+ }
456
+
449
457
  function printSummaryWithOptions(mode, tier, agents, preset, userInfo, detectedModels, missionControl) {
450
458
  const modelLabel =
451
459
  preset === 'detected'
@@ -542,8 +550,6 @@ export async function runInstall(options = {}) {
542
550
  console.log(` ${icons.info} ${dim('No model configured yet — you\'ll be prompted to choose one.')}\n`);
543
551
  }
544
552
 
545
- const allAgents = loadAgentRegistry();
546
-
547
553
  // 2. Install mode
548
554
  let mode;
549
555
  if (options.modeOverride) {
@@ -596,7 +602,9 @@ export async function runInstall(options = {}) {
596
602
 
597
603
  // 5. Tier selection
598
604
  let tier;
599
- if (auto) {
605
+ if (options.tierOverride) {
606
+ tier = normalizeTierOption(options.tierOverride);
607
+ } else if (auto) {
600
608
  tier = 'starter';
601
609
  } else {
602
610
  tier = await promptTier();
@@ -604,16 +612,49 @@ export async function runInstall(options = {}) {
604
612
 
605
613
  if (tier === 'pro') {
606
614
  const { checkProLicense, validateLicenseRemote, storeLicense, isValidFormat } = await import('./license.mjs');
615
+ const explicitLicenseKey = String(options.licenseKey || '').trim();
607
616
 
608
- const existing = await checkProLicense();
617
+ const existing = await checkProLicense({ revalidate: true });
609
618
 
610
619
  if (existing.licensed) {
611
- printStepDone(`Pro license found (${existing.key.slice(0, 12)}...)`);
620
+ if (existing.provisional) {
621
+ printStepDone(`Pro license found (${existing.key.slice(0, 12)}...)`, `temporary grace until ${existing.graceUntil}`);
622
+ } else if (existing.needsRevalidation) {
623
+ printStepDone(`Pro license found (${existing.key.slice(0, 12)}...)`, 'using cached validation while the service is unavailable');
624
+ } else {
625
+ printStepDone(`Pro license found (${existing.key.slice(0, 12)}...)`);
626
+ }
627
+ } else if (auto && explicitLicenseKey) {
628
+ if (!isValidFormat(explicitLicenseKey)) {
629
+ throw new Error('Invalid --license-key format. Key must be ALICE-XXXX-XXXX-XXXX.');
630
+ }
631
+
632
+ console.log(' Validating provided Pro license key...');
633
+ const result = await validateLicenseRemote(explicitLicenseKey);
634
+ if (result.valid) {
635
+ storeLicense(explicitLicenseKey, {
636
+ status: 'validated',
637
+ plan: result.plan || 'pro',
638
+ transport: result.transport,
639
+ source: 'remote',
640
+ });
641
+ printStepDone('License verified! Welcome to A.L.I.C.E. Pro.');
642
+ } else if (result.graceEligible) {
643
+ const graceRecord = storeLicense(explicitLicenseKey, {
644
+ status: 'grace',
645
+ plan: 'pro',
646
+ transport: result.transport,
647
+ source: 'grace',
648
+ });
649
+ printStepDone('Key stored', `temporary grace until ${graceRecord.graceUntil}`);
650
+ } else {
651
+ throw new Error(`Pro license validation failed: ${result.message ?? 'Not recognized'}`);
652
+ }
612
653
  } else if (auto) {
613
654
  // --yes flag: skip interactive prompt, fallback to Starter if no stored license
614
655
  console.log('');
615
656
  console.log(` ${icons.info} ${dim('Pro tier requires a license key.')}`);
616
- console.log(` ${dim('Run without --yes to enter your license key.')}`);
657
+ console.log(` ${dim('Run without --yes to enter your license key, or pass --license-key for automation.')}`);
617
658
  console.log(` ${dim('Falling back to Starter tier.')}`);
618
659
  console.log(` ${dim('Purchase a license at:')} ${cyan('https://getalice.av3.ai/pricing')}`);
619
660
  tier = 'starter';
@@ -635,12 +676,22 @@ export async function runInstall(options = {}) {
635
676
  const result = await validateLicenseRemote(key);
636
677
 
637
678
  if (result.valid) {
638
- storeLicense(key);
639
- if (result.message === 'offline') {
640
- printStepDone('Key stored', 'offline — will validate on next run');
641
- } else {
642
- printStepDone('License verified! Welcome to A.L.I.C.E. Pro.');
643
- }
679
+ storeLicense(key, {
680
+ status: 'validated',
681
+ plan: result.plan || 'pro',
682
+ transport: result.transport,
683
+ source: 'remote',
684
+ });
685
+ printStepDone('License verified! Welcome to A.L.I.C.E. Pro.');
686
+ break;
687
+ } else if (result.graceEligible) {
688
+ const graceRecord = storeLicense(key, {
689
+ status: 'grace',
690
+ plan: 'pro',
691
+ transport: result.transport,
692
+ source: 'grace',
693
+ });
694
+ printStepDone('Key stored', `temporary grace until ${graceRecord.graceUntil}`);
644
695
  break;
645
696
  } else {
646
697
  printStepFail(`Invalid key: ${result.message ?? 'Not recognized'}`);
@@ -684,12 +735,21 @@ export async function runInstall(options = {}) {
684
735
  dashboardUrl,
685
736
  ingestUrl,
686
737
  sourceNode,
738
+ teamId: String(options.cloudTeamId || existingMissionControl?.teamId || '').trim(),
739
+ teamSlug: String(options.cloudTeamSlug || existingMissionControl?.teamSlug || '').trim(),
740
+ teamName: String(options.cloudTeamName || existingMissionControl?.teamName || '').trim(),
741
+ teamPlan: String(options.cloudTeamPlan || existingMissionControl?.teamPlan || '').trim(),
687
742
  hasIngestToken: !!ingestToken,
688
743
  ingestToken,
689
744
  };
690
745
  }
691
746
  }
692
747
 
748
+ const codingAgent = resolveCodingAgentPreference({
749
+ detectedModels,
750
+ override: options.codingTool,
751
+ });
752
+ const allAgents = loadAgentRegistry(tier);
693
753
  const agents = allAgents;
694
754
 
695
755
  // 6. Confirmation
@@ -733,7 +793,7 @@ export async function runInstall(options = {}) {
733
793
  }
734
794
 
735
795
  // Scaffold workspaces
736
- const results = scaffoldAll(agents, userInfo);
796
+ const { workspaces: results, installedSkill } = scaffoldAll(agents, userInfo, codingAgent);
737
797
  let newWorkspaces = 0;
738
798
  let updatedWorkspaces = 0;
739
799
  for (const r of results) {
@@ -744,6 +804,7 @@ export async function runInstall(options = {}) {
744
804
  }
745
805
  }
746
806
  printStepDone('Workspaces', `${newWorkspaces} created, ${updatedWorkspaces} updated`);
807
+ printStepDone('Coding agent skill', `${installedSkill.preferred.name} preferred, ${installedSkill.fallback.name} fallback`);
747
808
 
748
809
  // Skills installation step
749
810
  const finalRuntimeForSkills = await detectRuntime();
@@ -762,6 +823,9 @@ export async function runInstall(options = {}) {
762
823
  userName: userInfo.name,
763
824
  userTimezone: userInfo.timezone,
764
825
  modelPreset: effectivePreset,
826
+ skills: [...new Set([...(existing?.skills || []), installedSkill.skillId, ...(skillsInstalled || [])])],
827
+ codingTool: installedSkill.preferredTool,
828
+ codingSkill: installedSkill.skillId,
765
829
  missionControl: missionControl
766
830
  ? {
767
831
  enabled: true,