zigrix 0.1.0-alpha.8 → 0.1.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.
Files changed (86) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +159 -120
  3. package/dist/agents/registry.js +19 -2
  4. package/dist/agents/roles.d.ts +10 -0
  5. package/dist/agents/roles.js +83 -0
  6. package/dist/config/defaults.d.ts +88 -6
  7. package/dist/config/defaults.js +82 -50
  8. package/dist/config/load.d.ts +5 -3
  9. package/dist/config/load.js +69 -30
  10. package/dist/config/schema.d.ts +46 -4
  11. package/dist/config/schema.js +49 -3
  12. package/dist/configure.d.ts +2 -0
  13. package/dist/configure.js +37 -14
  14. package/dist/dashboard/.next/BUILD_ID +1 -1
  15. package/dist/dashboard/.next/app-build-manifest.json +13 -13
  16. package/dist/dashboard/.next/app-path-routes-manifest.json +3 -3
  17. package/dist/dashboard/.next/build-manifest.json +2 -2
  18. package/dist/dashboard/.next/prerender-manifest.json +6 -6
  19. package/dist/dashboard/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  20. package/dist/dashboard/.next/server/app/_not-found.html +1 -1
  21. package/dist/dashboard/.next/server/app/_not-found.rsc +1 -1
  22. package/dist/dashboard/.next/server/app/api/auth/login/route.js +1 -1
  23. package/dist/dashboard/.next/server/app/api/auth/login/route_client-reference-manifest.js +1 -1
  24. package/dist/dashboard/.next/server/app/api/auth/logout/route.js +1 -1
  25. package/dist/dashboard/.next/server/app/api/auth/logout/route_client-reference-manifest.js +1 -1
  26. package/dist/dashboard/.next/server/app/api/auth/session/route.js +1 -1
  27. package/dist/dashboard/.next/server/app/api/auth/session/route_client-reference-manifest.js +1 -1
  28. package/dist/dashboard/.next/server/app/api/auth/setup/route.js +1 -1
  29. package/dist/dashboard/.next/server/app/api/auth/setup/route_client-reference-manifest.js +1 -1
  30. package/dist/dashboard/.next/server/app/api/overview/route_client-reference-manifest.js +1 -1
  31. package/dist/dashboard/.next/server/app/api/stream/route_client-reference-manifest.js +1 -1
  32. package/dist/dashboard/.next/server/app/api/tasks/[taskId]/cancel/route_client-reference-manifest.js +1 -1
  33. package/dist/dashboard/.next/server/app/api/tasks/[taskId]/conversation/route_client-reference-manifest.js +1 -1
  34. package/dist/dashboard/.next/server/app/api/tasks/[taskId]/route_client-reference-manifest.js +1 -1
  35. package/dist/dashboard/.next/server/app/login/page_client-reference-manifest.js +1 -1
  36. package/dist/dashboard/.next/server/app/login.html +1 -1
  37. package/dist/dashboard/.next/server/app/login.rsc +1 -1
  38. package/dist/dashboard/.next/server/app/page.js +2 -2
  39. package/dist/dashboard/.next/server/app/page_client-reference-manifest.js +1 -1
  40. package/dist/dashboard/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  41. package/dist/dashboard/.next/server/app/setup.html +1 -1
  42. package/dist/dashboard/.next/server/app/setup.rsc +1 -1
  43. package/dist/dashboard/.next/server/app-paths-manifest.json +3 -3
  44. package/dist/dashboard/.next/server/chunks/972.js +1 -1
  45. package/dist/dashboard/.next/server/functions-config-manifest.json +3 -3
  46. package/dist/dashboard/.next/server/middleware.js +1 -1
  47. package/dist/dashboard/.next/server/pages/404.html +1 -1
  48. package/dist/dashboard/.next/server/pages/500.html +1 -1
  49. package/dist/dashboard/.next/static/chunks/app/page-0314989c31e18b4b.js +1 -0
  50. package/dist/dashboard/.next/static/css/{94d75aff24d0c077.css → c3a7306cb2ba3f6c.css} +1 -1
  51. package/dist/dashboard.js +47 -0
  52. package/dist/doctor.js +28 -5
  53. package/dist/index.js +175 -171
  54. package/dist/onboard.d.ts +76 -2
  55. package/dist/onboard.js +529 -25
  56. package/dist/orchestration/dispatch.d.ts +3 -1
  57. package/dist/orchestration/dispatch.js +173 -45
  58. package/dist/orchestration/evidence.js +31 -4
  59. package/dist/orchestration/finalize.d.ts +1 -0
  60. package/dist/orchestration/finalize.js +5 -3
  61. package/dist/orchestration/report.js +9 -1
  62. package/dist/orchestration/worker.d.ts +1 -1
  63. package/dist/orchestration/worker.js +58 -8
  64. package/dist/rules/templates.js +3 -6
  65. package/dist/state/tasks.d.ts +12 -0
  66. package/dist/state/tasks.js +7 -0
  67. package/package.json +23 -2
  68. package/rules/defaults/README.md +9 -9
  69. package/rules/defaults/{back-zig.md → backend-agent.md} +4 -4
  70. package/rules/defaults/{front-zig.md → frontend-agent.md} +4 -4
  71. package/rules/defaults/orchestrator-agent.md +261 -0
  72. package/rules/defaults/{qa-zig.md → qa-agent.md} +11 -11
  73. package/rules/defaults/{sec-zig.md → security-agent.md} +4 -4
  74. package/rules/defaults/{sys-zig.md → system-agent.md} +8 -9
  75. package/rules/defaults/worker-common.md +25 -19
  76. package/skills/zigrix-doctor/SKILL.md +4 -2
  77. package/skills/zigrix-evidence/SKILL.md +7 -3
  78. package/skills/zigrix-main-agent-guide/SKILL.md +128 -0
  79. package/skills/zigrix-shared/SKILL.md +27 -3
  80. package/skills/zigrix-task-create/SKILL.md +8 -2
  81. package/skills/zigrix-task-status/SKILL.md +5 -2
  82. package/skills/zigrix-worker/SKILL.md +12 -4
  83. package/dist/dashboard/.next/static/chunks/app/page-25f54e54e74fb3af.js +0 -1
  84. package/rules/defaults/pro-zig.md +0 -238
  85. /package/dist/dashboard/.next/static/{2a4glWei05xr4Jg0Ly6cp → PT4hYxzrqxj-Zq4ZjtKNg}/_buildManifest.js +0 -0
  86. /package/dist/dashboard/.next/static/{2a4glWei05xr4Jg0Ly6cp → PT4hYxzrqxj-Zq4ZjtKNg}/_ssgManifest.js +0 -0
package/dist/onboard.js CHANGED
@@ -1,15 +1,20 @@
1
+ import { execSync } from 'node:child_process';
1
2
  import fs from 'node:fs';
3
+ import os from 'node:os';
2
4
  import path from 'node:path';
3
5
  import { fileURLToPath } from 'node:url';
4
6
  import { addAgent } from './agents/registry.js';
7
+ import { inferStandardAgentRole, STANDARD_AGENT_ROLES } from './agents/roles.js';
8
+ import { LEGACY_DEFAULT_GATEWAY_URL, resolveAbsolutePath } from './config/defaults.js';
5
9
  import { loadConfig, writeConfigFile, writeDefaultConfig } from './config/load.js';
6
10
  import { ensureBaseState, resolvePaths } from './state/paths.js';
7
11
  import { rebuildIndex } from './state/tasks.js';
8
12
  // ─── OpenClaw detection ───────────────────────────────────────────────────────
9
13
  export function detectOpenClawHome() {
14
+ const fallbackHome = process.env.HOME ?? os.homedir();
10
15
  return process.env.OPENCLAW_HOME
11
- ? path.resolve(process.env.OPENCLAW_HOME)
12
- : path.join(process.env.HOME ?? '~', '.openclaw');
16
+ ? resolveAbsolutePath(process.env.OPENCLAW_HOME)
17
+ : path.join(fallbackHome, '.openclaw');
13
18
  }
14
19
  export function loadOpenClawConfig(openclawHome) {
15
20
  const configPath = path.join(openclawHome, 'openclaw.json');
@@ -23,12 +28,120 @@ export function loadOpenClawConfig(openclawHome) {
23
28
  return null;
24
29
  }
25
30
  }
31
+ function normalizeGatewayUrl(input) {
32
+ const trimmed = input?.trim() ?? '';
33
+ if (!trimmed)
34
+ return '';
35
+ const withScheme = /^[a-z]+:\/\//i.test(trimmed) ? trimmed : `http://${trimmed}`;
36
+ const parsed = new URL(withScheme);
37
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
38
+ throw new Error(`unsupported gateway url protocol: ${parsed.protocol}`);
39
+ }
40
+ return parsed.toString().replace(/\/+$/, '');
41
+ }
42
+ function coerceGatewayPort(value) {
43
+ const n = typeof value === 'string' && value.trim().length > 0 ? Number(value) : typeof value === 'number' ? value : NaN;
44
+ if (!Number.isFinite(n) || n <= 0 || n > 65535)
45
+ return null;
46
+ return Math.trunc(n);
47
+ }
48
+ export function resolveOpenClawGatewayUrl(openclawConfig) {
49
+ if (!openclawConfig || typeof openclawConfig !== 'object')
50
+ return null;
51
+ const directUrl = openclawConfig.gateway?.url;
52
+ if (typeof directUrl === 'string' && directUrl.trim().length > 0) {
53
+ return normalizeGatewayUrl(directUrl);
54
+ }
55
+ const port = coerceGatewayPort(openclawConfig.gateway?.port);
56
+ if (port) {
57
+ return `http://127.0.0.1:${port}`;
58
+ }
59
+ return null;
60
+ }
61
+ export function resolveOpenClawGatewayToken(openclawConfig) {
62
+ const token = openclawConfig?.gateway?.auth?.token;
63
+ return typeof token === 'string' && token.trim().length > 0 ? token : null;
64
+ }
65
+ function shouldReplaceStoredGatewayUrl(stored, detected) {
66
+ const trimmed = stored.trim();
67
+ if (!trimmed)
68
+ return Boolean(detected);
69
+ if (!detected)
70
+ return false;
71
+ return trimmed === LEGACY_DEFAULT_GATEWAY_URL;
72
+ }
73
+ /**
74
+ * Discover the openclaw binary path.
75
+ * Strategy order:
76
+ * 1. `which openclaw` — available in current PATH
77
+ * 2. Common nvm/volta/fnm managed paths
78
+ * 3. Well-known locations: /usr/local/bin, ~/.local/bin
79
+ * 4. Walk node_modules/.bin from openclaw home
80
+ *
81
+ * Returns the resolved absolute path or null.
82
+ */
83
+ export function resolveOpenClawBin(openclawHome) {
84
+ // Strategy 1: which/where
85
+ try {
86
+ const result = execSync('which openclaw', { encoding: 'utf8', timeout: 3000 }).trim();
87
+ if (result && fs.existsSync(result))
88
+ return path.resolve(result);
89
+ }
90
+ catch {
91
+ // not in PATH
92
+ }
93
+ // Strategy 2: nvm/volta/fnm managed global bin dirs
94
+ const home = process.env.HOME ?? '';
95
+ const nodeVersion = process.versions.node;
96
+ const majorMinorPatch = nodeVersion; // e.g., "25.5.0"
97
+ const nvmCandidate = path.join(home, '.nvm', 'versions', 'node', `v${majorMinorPatch}`, 'bin', 'openclaw');
98
+ if (fs.existsSync(nvmCandidate))
99
+ return path.resolve(nvmCandidate);
100
+ // Also try nvm current symlink
101
+ const nvmCurrent = path.join(home, '.nvm', 'current', 'bin', 'openclaw');
102
+ if (fs.existsSync(nvmCurrent))
103
+ return path.resolve(nvmCurrent);
104
+ // Volta
105
+ const voltaCandidate = path.join(home, '.volta', 'bin', 'openclaw');
106
+ if (fs.existsSync(voltaCandidate))
107
+ return path.resolve(voltaCandidate);
108
+ // fnm
109
+ const fnmCandidate = path.join(home, '.fnm', 'node-versions', `v${majorMinorPatch}`, 'installation', 'bin', 'openclaw');
110
+ if (fs.existsSync(fnmCandidate))
111
+ return path.resolve(fnmCandidate);
112
+ // Strategy 3: well-known system paths
113
+ for (const dir of ['/usr/local/bin', path.join(home, '.local', 'bin')]) {
114
+ const candidate = path.join(dir, 'openclaw');
115
+ if (fs.existsSync(candidate))
116
+ return path.resolve(candidate);
117
+ }
118
+ // Strategy 4: node global lib path (npm root -g)
119
+ try {
120
+ const globalRoot = execSync('npm root -g', { encoding: 'utf8', timeout: 3000 }).trim();
121
+ if (globalRoot) {
122
+ const candidate = path.join(path.dirname(globalRoot), 'bin', 'openclaw');
123
+ if (fs.existsSync(candidate))
124
+ return path.resolve(candidate);
125
+ }
126
+ }
127
+ catch {
128
+ // npm not available or timed out
129
+ }
130
+ // Strategy 5: from openclaw home
131
+ if (openclawHome) {
132
+ // Some installations place a bin reference inside the home dir
133
+ const homeBin = path.join(openclawHome, 'bin', 'openclaw');
134
+ if (fs.existsSync(homeBin))
135
+ return path.resolve(homeBin);
136
+ }
137
+ return null;
138
+ }
26
139
  // ─── Agent filtering ──────────────────────────────────────────────────────────
27
140
  export function filterAgents(agents) {
28
141
  return agents.filter((a) => a.id !== 'main');
29
142
  }
30
143
  // ─── Agent registration ───────────────────────────────────────────────────────
31
- export function registerAgents(config, agents) {
144
+ export function registerAgents(config, agents, roleAssignments) {
32
145
  let current = structuredClone(config);
33
146
  const registered = [];
34
147
  const skipped = [];
@@ -38,7 +151,10 @@ export function registerAgents(config, agents) {
38
151
  skipped.push(agent.id);
39
152
  continue;
40
153
  }
41
- const role = agent.identity?.theme ?? 'assistant';
154
+ const role = roleAssignments?.[agent.id] ?? inferStandardAgentRole({
155
+ agentId: agent.id,
156
+ theme: agent.identity?.theme ?? null,
157
+ });
42
158
  const result = addAgent(current, {
43
159
  id: agent.id,
44
160
  role,
@@ -73,6 +189,28 @@ export function resolveBundledRulesDir() {
73
189
  }
74
190
  return null;
75
191
  }
192
+ export function resolveRuleSeedSource(projectDir) {
193
+ const searched = [];
194
+ const base = projectDir?.trim();
195
+ if (base) {
196
+ const resolvedBase = resolveAbsolutePath(base);
197
+ const candidates = [
198
+ path.join(resolvedBase, 'rules', 'defaults'),
199
+ path.join(resolvedBase, 'rules'),
200
+ ];
201
+ searched.push(...candidates);
202
+ for (const candidate of candidates) {
203
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
204
+ return { sourceDir: candidate, source: 'external', searched };
205
+ }
206
+ }
207
+ }
208
+ const bundled = resolveBundledRulesDir();
209
+ if (bundled) {
210
+ return { sourceDir: bundled, source: 'bundled', searched };
211
+ }
212
+ return { sourceDir: null, source: 'none', searched };
213
+ }
76
214
  export function seedRules(sourceDir, targetDir) {
77
215
  const copied = [];
78
216
  const skipped = [];
@@ -102,9 +240,9 @@ export function seedRules(sourceDir, targetDir) {
102
240
  return { copied, skipped, source: effectiveSource === sourceDir ? 'external' : 'bundled' };
103
241
  }
104
242
  // ─── PATH check and stabilization ─────────────────────────────────────────────
105
- export function checkZigrixInPath() {
106
- const pathEnv = process.env.PATH ?? '';
107
- const dirs = pathEnv.split(path.delimiter).filter(Boolean);
243
+ export const STABLE_SHELL_PATHS = ['/usr/local/bin', '/usr/bin', '/bin', '/usr/sbin', '/sbin'];
244
+ export function checkZigrixInPath(opts) {
245
+ const dirs = opts?._overrideStablePaths ?? STABLE_SHELL_PATHS;
108
246
  for (const dir of dirs) {
109
247
  try {
110
248
  if (!fs.existsSync(dir))
@@ -215,9 +353,10 @@ export function findUserBinDir() {
215
353
  * 2. ~/.local/bin — user-local fallback; may not be in PATH, shows warning if so
216
354
  *
217
355
  * @param opts._overrideSystemBinDir - Override system bin dir selection (for testing)
356
+ * @param opts._overrideStablePaths - Override stable paths list (for testing)
218
357
  */
219
358
  export function ensureZigrixInPath(opts) {
220
- if (checkZigrixInPath()) {
359
+ if (checkZigrixInPath({ _overrideStablePaths: opts?._overrideStablePaths })) {
221
360
  return { alreadyInPath: true, symlinkCreated: false, symlinkPath: null, warning: null };
222
361
  }
223
362
  const binEntry = resolveZigrixBin();
@@ -249,7 +388,15 @@ export function ensureZigrixInPath(opts) {
249
388
  fs.chmodSync(symlinkPath, 0o755);
250
389
  return { alreadyInPath: false, symlinkCreated: true, symlinkPath, warning: null };
251
390
  }
252
- catch {
391
+ catch (e) {
392
+ if (e.code === 'EACCES') {
393
+ return {
394
+ alreadyInPath: false,
395
+ symlinkCreated: false,
396
+ symlinkPath: null,
397
+ warning: `Cannot write to ${systemBinDir} (permission denied). Run:\n sudo ln -sfn $(which zigrix) ${symlinkPath}`,
398
+ };
399
+ }
253
400
  // Fall through to user bin dir
254
401
  }
255
402
  }
@@ -302,6 +449,121 @@ export function ensureZigrixInPath(opts) {
302
449
  : `Created zigrix at ${symlinkPath}, but ${userBinDir} is not in your PATH. Add it:\n export PATH="${userBinDir}:$PATH"`;
303
450
  return { alreadyInPath: false, symlinkCreated: true, symlinkPath, warning };
304
451
  }
452
+ // ─── OpenClaw PATH check and stabilization ────────────────────────────────────
453
+ export function checkOpenClawInPath(opts) {
454
+ const dirs = opts?._overrideStablePaths ?? STABLE_SHELL_PATHS;
455
+ for (const dir of dirs) {
456
+ try {
457
+ if (!fs.existsSync(dir))
458
+ continue;
459
+ const entries = fs.readdirSync(dir);
460
+ if (entries.includes('openclaw')) {
461
+ return true;
462
+ }
463
+ }
464
+ catch {
465
+ // skip unreadable dirs
466
+ }
467
+ }
468
+ return false;
469
+ }
470
+ /**
471
+ * Ensure openclaw is reachable from PATH.
472
+ * Same strategy as ensureZigrixInPath():
473
+ * 1. /usr/local/bin — stable path, accessible from non-login shells
474
+ * 2. ~/.local/bin — user-local fallback
475
+ *
476
+ * @param opts._overrideSystemBinDir - Override system bin dir selection (for testing)
477
+ * @param opts._overrideStablePaths - Override stable paths list (for testing)
478
+ */
479
+ export function ensureOpenClawInPath(opts) {
480
+ if (checkOpenClawInPath({ _overrideStablePaths: opts?._overrideStablePaths })) {
481
+ return { alreadyInPath: true, symlinkCreated: false, symlinkPath: null, warning: null };
482
+ }
483
+ const binPath = resolveOpenClawBin();
484
+ if (!binPath) {
485
+ return {
486
+ alreadyInPath: false,
487
+ symlinkCreated: false,
488
+ symlinkPath: null,
489
+ warning: 'Could not locate openclaw binary. Ensure openclaw is installed via npm.',
490
+ };
491
+ }
492
+ // ── Strategy 1: system bin dir (accessible from non-login shells) ──────────
493
+ const systemBinDir = opts !== undefined && '_overrideSystemBinDir' in opts
494
+ ? opts._overrideSystemBinDir
495
+ : findSystemBinDir();
496
+ if (systemBinDir) {
497
+ const symlinkPath = path.join(systemBinDir, 'openclaw');
498
+ try {
499
+ // Remove stale entry if present
500
+ try {
501
+ if (fs.existsSync(symlinkPath) || fs.lstatSync(symlinkPath).isSymbolicLink()) {
502
+ fs.unlinkSync(symlinkPath);
503
+ }
504
+ }
505
+ catch {
506
+ // doesn't exist — fine
507
+ }
508
+ fs.symlinkSync(binPath, symlinkPath);
509
+ fs.chmodSync(symlinkPath, 0o755);
510
+ return { alreadyInPath: false, symlinkCreated: true, symlinkPath, warning: null };
511
+ }
512
+ catch (e) {
513
+ if (e.code === 'EACCES') {
514
+ return {
515
+ alreadyInPath: false,
516
+ symlinkCreated: false,
517
+ symlinkPath: null,
518
+ warning: `Cannot write to ${systemBinDir} (permission denied). Run:\n sudo ln -sfn ${binPath} ${symlinkPath}`,
519
+ };
520
+ }
521
+ // Fall through to user bin dir
522
+ }
523
+ }
524
+ // ── Strategy 2: user-local bin dir ────────────────────────────────────────
525
+ const userBinDir = findUserBinDir();
526
+ try {
527
+ fs.mkdirSync(userBinDir, { recursive: true });
528
+ }
529
+ catch {
530
+ return {
531
+ alreadyInPath: false,
532
+ symlinkCreated: false,
533
+ symlinkPath: null,
534
+ warning: `Could not create ${userBinDir}. Create it manually and add to PATH.`,
535
+ };
536
+ }
537
+ const symlinkPath = path.join(userBinDir, 'openclaw');
538
+ try {
539
+ // Remove existing if present (stale symlink)
540
+ if (fs.existsSync(symlinkPath) || fs.lstatSync(symlinkPath).isSymbolicLink()) {
541
+ fs.unlinkSync(symlinkPath);
542
+ }
543
+ }
544
+ catch {
545
+ // doesn't exist, fine
546
+ }
547
+ try {
548
+ fs.symlinkSync(binPath, symlinkPath);
549
+ fs.chmodSync(symlinkPath, 0o755);
550
+ }
551
+ catch (e) {
552
+ return {
553
+ alreadyInPath: false,
554
+ symlinkCreated: false,
555
+ symlinkPath: null,
556
+ warning: `Failed to create symlink at ${symlinkPath}: ${e instanceof Error ? e.message : String(e)}`,
557
+ };
558
+ }
559
+ // Check if userBinDir is actually in PATH
560
+ const pathEnv = process.env.PATH ?? '';
561
+ const inPath = pathEnv.split(path.delimiter).some((d) => path.resolve(d) === path.resolve(userBinDir));
562
+ const warning = inPath
563
+ ? null
564
+ : `Created openclaw at ${symlinkPath}, but ${userBinDir} is not in your PATH. Add it:\n export PATH="${userBinDir}:$PATH"`;
565
+ return { alreadyInPath: false, symlinkCreated: true, symlinkPath, warning };
566
+ }
305
567
  // ─── OpenClaw skill registration ──────────────────────────────────────────────
306
568
  /**
307
569
  * Find the skills/ directory bundled with this zigrix package.
@@ -418,13 +680,137 @@ export async function promptAgentSelection(agents) {
418
680
  return agents;
419
681
  }
420
682
  }
683
+ export async function promptAgentRoleAssignments(agents) {
684
+ const defaults = Object.fromEntries(agents.map((agent) => [agent.id, inferStandardAgentRole({ agentId: agent.id, theme: agent.identity?.theme ?? null })]));
685
+ if (agents.length === 0)
686
+ return defaults;
687
+ try {
688
+ const { checkbox } = await import('@inquirer/prompts');
689
+ const assignments = {};
690
+ for (const agent of agents) {
691
+ const suggested = defaults[agent.id];
692
+ let selected = [];
693
+ while (selected.length !== 1) {
694
+ selected = await checkbox({
695
+ message: `Assign role for ${agent.id} (space toggle + enter confirm, choose exactly one):`,
696
+ choices: STANDARD_AGENT_ROLES.map((role) => ({
697
+ name: `${role}${role === suggested ? ' (suggested)' : ''}`,
698
+ value: role,
699
+ checked: role === suggested,
700
+ })),
701
+ });
702
+ if (selected.length !== 1) {
703
+ console.log(`⚠️ Select exactly one role for ${agent.id}.`);
704
+ }
705
+ }
706
+ assignments[agent.id] = selected[0];
707
+ }
708
+ return assignments;
709
+ }
710
+ catch {
711
+ console.log('ℹ️ Non-interactive mode — inferring roles from agent ids/themes.');
712
+ return defaults;
713
+ }
714
+ }
715
+ export async function promptWorkspaceBaseDir(defaultPath) {
716
+ try {
717
+ const { input } = await import('@inquirer/prompts');
718
+ const answer = await input({
719
+ message: 'Workspace directory',
720
+ default: defaultPath,
721
+ validate: (value) => {
722
+ if (!value || value.trim().length === 0)
723
+ return 'Workspace directory is required';
724
+ return true;
725
+ },
726
+ });
727
+ return resolveAbsolutePath((answer || defaultPath).trim());
728
+ }
729
+ catch {
730
+ console.log('ℹ️ Non-interactive mode — using configured workspace directory.');
731
+ return resolveAbsolutePath(defaultPath);
732
+ }
733
+ }
734
+ export async function promptGatewayUrl(defaultValue) {
735
+ try {
736
+ const { input } = await import('@inquirer/prompts');
737
+ const answer = await input({
738
+ message: 'OpenClaw gateway URL (leave blank to skip)',
739
+ default: defaultValue,
740
+ validate: (value) => {
741
+ const trimmed = value.trim();
742
+ if (!trimmed)
743
+ return true;
744
+ try {
745
+ normalizeGatewayUrl(trimmed);
746
+ return true;
747
+ }
748
+ catch (error) {
749
+ return error.message;
750
+ }
751
+ },
752
+ });
753
+ return normalizeGatewayUrl((answer || defaultValue).trim());
754
+ }
755
+ catch {
756
+ console.log('ℹ️ Non-interactive mode — skipping gateway URL prompt.');
757
+ return normalizeGatewayUrl(defaultValue);
758
+ }
759
+ }
760
+ export function ensureOrchestratorId(config, preferredId) {
761
+ const next = structuredClone(config);
762
+ const participants = new Set(next.agents.orchestration.participants);
763
+ const excluded = new Set(next.agents.orchestration.excluded);
764
+ const participantMode = participants.size > 0;
765
+ const eligible = Object.entries(next.agents.registry)
766
+ .filter(([agentId, agent]) => {
767
+ if (!agent.enabled)
768
+ return false;
769
+ if (agent.role !== 'orchestrator')
770
+ return false;
771
+ if (excluded.has(agentId))
772
+ return false;
773
+ if (participantMode && !participants.has(agentId))
774
+ return false;
775
+ return true;
776
+ })
777
+ .map(([agentId]) => agentId)
778
+ .sort();
779
+ const requested = preferredId?.trim();
780
+ const current = next.agents.orchestration.orchestratorId;
781
+ if (requested) {
782
+ if (!eligible.includes(requested)) {
783
+ return {
784
+ config: next,
785
+ changed: false,
786
+ warning: `requested orchestrator '${requested}' is not eligible (eligible: ${eligible.join(', ') || 'none'})`,
787
+ };
788
+ }
789
+ const changed = current !== requested;
790
+ next.agents.orchestration.orchestratorId = requested;
791
+ return { config: next, changed };
792
+ }
793
+ if (eligible.includes(current)) {
794
+ return { config: next, changed: false };
795
+ }
796
+ const fallback = eligible[0];
797
+ if (!fallback) {
798
+ return {
799
+ config: next,
800
+ changed: false,
801
+ warning: 'no eligible orchestrator role agent found. set one with --orchestrator-id after assigning roles.',
802
+ };
803
+ }
804
+ next.agents.orchestration.orchestratorId = fallback;
805
+ return { config: next, changed: true };
806
+ }
421
807
  // ─── Ensure config (idempotent) ───────────────────────────────────────────────
422
808
  function ensureConfig() {
423
809
  const existing = loadConfig({});
424
810
  if (existing.configPath && fs.existsSync(existing.configPath)) {
425
811
  return { configPath: existing.configPath, isNew: false };
426
812
  }
427
- const configPath = writeDefaultConfig(undefined, false);
813
+ const configPath = writeDefaultConfig(false);
428
814
  return { configPath, isNew: true };
429
815
  }
430
816
  // ─── Main onboard function ────────────────────────────────────────────────────
@@ -435,10 +821,22 @@ export async function runOnboard(options) {
435
821
  if (!silent)
436
822
  console.log(msg);
437
823
  };
438
- // 1. Ensure ~/.zigrix base state (idempotent)
824
+ // 1. Ensure config.paths.baseDir state (idempotent)
439
825
  const { configPath } = ensureConfig();
440
826
  const loaded = loadConfig({ configPath });
441
- const paths = resolvePaths(loaded.config);
827
+ let nextConfig = structuredClone(loaded.config);
828
+ const configuredWorkspace = nextConfig.workspace.projectsBaseDir?.trim() || path.join(nextConfig.paths.baseDir, 'workspace');
829
+ const desiredWorkspace = options.projectsBaseDir
830
+ ? resolveAbsolutePath(options.projectsBaseDir)
831
+ : options.yes
832
+ ? resolveAbsolutePath(configuredWorkspace)
833
+ : await promptWorkspaceBaseDir(configuredWorkspace);
834
+ if (nextConfig.workspace.projectsBaseDir !== desiredWorkspace) {
835
+ nextConfig.workspace.projectsBaseDir = desiredWorkspace;
836
+ writeConfigFile(configPath, nextConfig);
837
+ log(`✅ Workspace base dir set to: ${desiredWorkspace}`);
838
+ }
839
+ const paths = resolvePaths(nextConfig);
442
840
  ensureBaseState(paths);
443
841
  rebuildIndex(paths);
444
842
  // 2. Detect OpenClaw
@@ -456,6 +854,23 @@ export async function runOnboard(options) {
456
854
  log(`⚠️ ${warnings[warnings.length - 1]}`);
457
855
  }
458
856
  }
857
+ const storedGatewayUrl = nextConfig.openclaw.gatewayUrl?.trim() ?? '';
858
+ const detectedGatewayUrl = resolveOpenClawGatewayUrl(openclawConfig);
859
+ const resolvedGatewayUrl = (() => {
860
+ if (typeof options.gatewayUrl === 'string' && options.gatewayUrl.trim().length > 0) {
861
+ return normalizeGatewayUrl(options.gatewayUrl);
862
+ }
863
+ if (storedGatewayUrl && !shouldReplaceStoredGatewayUrl(storedGatewayUrl, detectedGatewayUrl)) {
864
+ return normalizeGatewayUrl(storedGatewayUrl);
865
+ }
866
+ if (detectedGatewayUrl) {
867
+ return detectedGatewayUrl;
868
+ }
869
+ if (openclawExists && !options.yes) {
870
+ return '';
871
+ }
872
+ return storedGatewayUrl ? normalizeGatewayUrl(storedGatewayUrl) : '';
873
+ })();
459
874
  // 3. Agent selection and registration
460
875
  let agentsRegistered = [];
461
876
  let agentsSkipped = [];
@@ -472,32 +887,64 @@ export async function runOnboard(options) {
472
887
  selectedAgents = await promptAgentSelection(allAgents);
473
888
  }
474
889
  if (selectedAgents.length > 0) {
475
- const result = registerAgents(loaded.config, selectedAgents);
890
+ const roleAssignments = options.yes
891
+ ? Object.fromEntries(selectedAgents.map((agent) => [agent.id, inferStandardAgentRole({ agentId: agent.id, theme: agent.identity?.theme ?? null })]))
892
+ : await promptAgentRoleAssignments(selectedAgents);
893
+ const result = registerAgents(nextConfig, selectedAgents, roleAssignments);
894
+ nextConfig = result.config;
476
895
  agentsRegistered = result.registered;
477
896
  agentsSkipped = result.skipped;
478
897
  if (agentsRegistered.length > 0) {
479
- writeConfigFile(configPath, result.config);
480
898
  log(`✅ Registered agents: ${agentsRegistered.join(', ')}`);
481
899
  }
482
900
  if (agentsSkipped.length > 0) {
483
901
  log(`⏭️ Already registered (skipped): ${agentsSkipped.join(', ')}`);
484
902
  }
485
903
  }
904
+ const orchResult = ensureOrchestratorId(nextConfig, options.orchestratorId);
905
+ nextConfig = orchResult.config;
906
+ if (orchResult.warning) {
907
+ warnings.push(orchResult.warning);
908
+ log(`⚠️ ${orchResult.warning}`);
909
+ }
910
+ if (agentsRegistered.length > 0 || orchResult.changed) {
911
+ writeConfigFile(configPath, nextConfig);
912
+ if (orchResult.changed) {
913
+ log(`✅ Orchestrator set to: ${nextConfig.agents.orchestration.orchestratorId}`);
914
+ }
915
+ }
916
+ }
917
+ if (!openclawConfig && options.orchestratorId) {
918
+ const orchResult = ensureOrchestratorId(nextConfig, options.orchestratorId);
919
+ if (orchResult.warning) {
920
+ warnings.push(orchResult.warning);
921
+ log(`⚠️ ${orchResult.warning}`);
922
+ }
923
+ if (orchResult.changed) {
924
+ writeConfigFile(configPath, orchResult.config);
925
+ log(`✅ Orchestrator set to: ${orchResult.config.agents.orchestration.orchestratorId}`);
926
+ }
486
927
  }
487
928
  // 4. Seed rules
488
- const projectDir = options.projectDir ?? process.cwd();
489
- const rulesSourceDir = path.join(projectDir, 'orchestration', 'rules');
490
929
  const rulesTargetDir = paths.rulesDir;
491
- if (!fs.existsSync(rulesSourceDir)) {
492
- warnings.push(`orchestration/rules/ not found at ${rulesSourceDir}. Rules seeding skipped.`);
930
+ const ruleSeed = resolveRuleSeedSource(options.projectDir);
931
+ let rulesCopied = [];
932
+ let rulesSkipped = [];
933
+ if (!ruleSeed.sourceDir) {
934
+ warnings.push('No bundled rule templates available. Rules seeding skipped.');
493
935
  log(`⚠️ ${warnings[warnings.length - 1]}`);
494
936
  }
495
- const { copied: rulesCopied, skipped: rulesSkipped } = seedRules(rulesSourceDir, rulesTargetDir);
496
- if (rulesCopied.length > 0) {
497
- log(`✅ Rules copied: ${rulesCopied.join(', ')}`);
498
- }
499
- if (rulesSkipped.length > 0) {
500
- log(`⏭️ Rules already exist (skipped): ${rulesSkipped.join(', ')}`);
937
+ else {
938
+ if (options.projectDir && ruleSeed.source === 'bundled') {
939
+ log(`ℹ️ No external rule templates found under ${options.projectDir}; using bundled defaults.`);
940
+ }
941
+ ({ copied: rulesCopied, skipped: rulesSkipped } = seedRules(ruleSeed.sourceDir, rulesTargetDir));
942
+ if (rulesCopied.length > 0) {
943
+ log(`✅ Rules copied: ${rulesCopied.join(', ')}`);
944
+ }
945
+ if (rulesSkipped.length > 0) {
946
+ log(`⏭️ Rules already exist (skipped): ${rulesSkipped.join(', ')}`);
947
+ }
501
948
  }
502
949
  // 5. Stabilize PATH — ensure zigrix is reachable
503
950
  const pathResult = ensureZigrixInPath();
@@ -511,7 +958,59 @@ export async function runOnboard(options) {
511
958
  warnings.push(pathResult.warning);
512
959
  log(`⚠️ ${pathResult.warning}`);
513
960
  }
514
- // 6. Register OpenClaw skills (symlink skill packs)
961
+ // 6. Detect and persist openclaw binary path
962
+ let openclawBinPath = null;
963
+ if (openclawExists) {
964
+ openclawBinPath = resolveOpenClawBin(openclawHome);
965
+ if (openclawBinPath) {
966
+ log(`✅ OpenClaw binary found: ${openclawBinPath}`);
967
+ }
968
+ else {
969
+ warnings.push('openclaw binary not found. Dashboard conversation features may be limited.');
970
+ log(`⚠️ ${warnings[warnings.length - 1]}`);
971
+ }
972
+ // Persist openclaw integration config (home, binPath, gatewayUrl)
973
+ const currentConfig = loadConfig({ configPath }).config;
974
+ // Gateway URL: explicit flag > autodiscovery > interactive prompt > stored value
975
+ let finalGatewayUrl = resolvedGatewayUrl;
976
+ if (!finalGatewayUrl && openclawExists && !options.yes) {
977
+ const suggested = detectedGatewayUrl || '';
978
+ finalGatewayUrl = await promptGatewayUrl(suggested);
979
+ }
980
+ if (finalGatewayUrl) {
981
+ log(`✅ OpenClaw gateway URL: ${finalGatewayUrl}`);
982
+ }
983
+ else if (openclawExists) {
984
+ warnings.push('OpenClaw gateway URL not configured. Dashboard gateway features may be limited. Set later with `zigrix configure --section openclaw` or `zigrix onboard --gateway-url <url>`.');
985
+ log(`⚠️ ${warnings[warnings.length - 1]}`);
986
+ }
987
+ const needsUpdate = currentConfig.openclaw.home !== openclawHome ||
988
+ currentConfig.openclaw.binPath !== openclawBinPath ||
989
+ currentConfig.openclaw.gatewayUrl !== (finalGatewayUrl || '');
990
+ if (needsUpdate) {
991
+ currentConfig.openclaw.home = openclawHome;
992
+ currentConfig.openclaw.binPath = openclawBinPath;
993
+ currentConfig.openclaw.gatewayUrl = finalGatewayUrl || '';
994
+ writeConfigFile(configPath, currentConfig);
995
+ log(`✅ OpenClaw config persisted (home: ${openclawHome}, bin: ${openclawBinPath ?? 'not found'}, gateway: ${finalGatewayUrl || 'not set'})`);
996
+ }
997
+ }
998
+ // 7. Stabilize PATH — ensure openclaw is reachable from non-login shells
999
+ let openclawPathResult = { alreadyInPath: false, symlinkCreated: false, symlinkPath: null, warning: null };
1000
+ if (openclawBinPath) {
1001
+ openclawPathResult = ensureOpenClawInPath();
1002
+ if (openclawPathResult.symlinkCreated) {
1003
+ log(`✅ openclaw symlinked to ${openclawPathResult.symlinkPath}`);
1004
+ }
1005
+ else if (openclawPathResult.alreadyInPath) {
1006
+ log('✅ openclaw already in PATH');
1007
+ }
1008
+ if (openclawPathResult.warning) {
1009
+ warnings.push(openclawPathResult.warning);
1010
+ log(`⚠️ ${openclawPathResult.warning}`);
1011
+ }
1012
+ }
1013
+ // 8. Register OpenClaw skills (symlink skill packs)
515
1014
  let skillsRegistered = [];
516
1015
  let skillsSkipped = [];
517
1016
  let skillsFailed = [];
@@ -542,10 +1041,13 @@ export async function runOnboard(options) {
542
1041
  ok: true,
543
1042
  action: 'onboard',
544
1043
  baseDir: paths.baseDir,
1044
+ workspaceBaseDir: nextConfig.workspace.projectsBaseDir,
545
1045
  configPath,
546
1046
  paths,
547
1047
  openclawDetected: openclawExists,
548
1048
  openclawHome,
1049
+ openclawBinPath,
1050
+ openclawGatewayUrl: resolvedGatewayUrl || null,
549
1051
  agentsRegistered,
550
1052
  agentsSkipped,
551
1053
  rulesCopied,
@@ -554,9 +1056,11 @@ export async function runOnboard(options) {
554
1056
  skillsSkipped,
555
1057
  skillsFailed,
556
1058
  pathStabilized: pathResult,
1059
+ openclawPathStabilized: openclawPathResult,
557
1060
  warnings,
558
1061
  checks: {
559
1062
  zigrixInPath: pathResult.alreadyInPath || pathResult.symlinkCreated,
1063
+ openclawInPath: openclawPathResult.alreadyInPath || openclawPathResult.symlinkCreated,
560
1064
  openclawSkillsDir: openclawSkillsDirExists,
561
1065
  },
562
1066
  };