@ghl-ai/aw 0.1.49-beta.0 → 0.1.50-beta.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.
@@ -12,9 +12,9 @@
12
12
  # - .codex/scripts/codex-web-bootstrap.sh (passes --harness codex-web)
13
13
  #
14
14
  # Override knobs (env):
15
- # AW_PACKAGE npm spec to install. Defaults to @ghl-ai/aw@latest. Override
16
- # with @ghl-ai/aw@beta to opt into pre-release builds, or pin to
17
- # @ghl-ai/aw@0.1.x for reproducible CI runs.
15
+ # AW_PACKAGE npm spec to install. Defaults to @ghl-ai/aw@latest.
16
+ # Override to pin a specific @ghl-ai/aw@0.1.x for reproducible
17
+ # CI runs.
18
18
  set -Eeuo pipefail
19
19
 
20
20
  # GitHub auth is resolved inside `aw c4` so GITHUB_PAT, GITHUB_TOKEN, and
package/cli.mjs CHANGED
@@ -89,9 +89,11 @@ function printHelp() {
89
89
  sec('Setup'),
90
90
  cmd('aw init', 'Initialize workspace (auto-installs suggested integrations)'),
91
91
  cmd('aw init --namespace <team/sub-team>', 'Add a team namespace (optional)'),
92
+ cmd('aw init skills <path...>', 'Initialize/link only specific skill folders'),
92
93
  cmd('aw init --no-integrations', 'Skip integration setup (Codex, Caveman, Graphify, etc)'),
93
94
  ` ${chalk.dim('Teams: platform, revex, mobile, commerce, leadgen, crm, marketplace, ai')}`,
94
95
  ` ${chalk.dim('Example: aw init --namespace revex/courses')}`,
96
+ ` ${chalk.dim('Example: aw init skills platform/core/skills/pr-review')}`,
95
97
  cmd('aw init-repo', 'Scaffold cloud-bootstrap files (idempotent, --dry-run/--force/--diff)'),
96
98
 
97
99
  sec('Download'),
@@ -152,6 +154,7 @@ function printHelp() {
152
154
  cmd('aw pull <team>/agents', 'All agents from a team'),
153
155
  cmd('aw pull <team>/agents/<name>', 'One specific agent'),
154
156
  cmd('aw pull <team>/skills/<name>', 'One specific skill folder'),
157
+ cmd('aw init skills <team>/<path>/skills/<name>', 'Install and link only selected skill symlinks'),
155
158
  '',
156
159
  ` ${chalk.dim('# Push your local changes to registry')}`,
157
160
  cmd('aw push', 'Push all modified files (one PR)'),
package/commands/c4.mjs CHANGED
@@ -350,10 +350,10 @@ export async function c4Command(rawArgs, overrides = {}) {
350
350
  return exit(0);
351
351
  }
352
352
 
353
- // Step 6 — npm install -g @ghl-ai/aw.
354
- const npmRes = spawnSync('npm', ['install', '-g', '@ghl-ai/aw'], { stdio: 'pipe' });
353
+ // Step 6 — npm install -g @ghl-ai/aw@latest.
354
+ const npmRes = spawnSync('npm', ['install', '-g', '@ghl-ai/aw@latest'], { stdio: 'pipe' });
355
355
  if (npmRes && npmRes.status !== 0) {
356
- writer.stderr('[aw-c4] npm install -g @ghl-ai/aw failed (non-fatal); using existing aw if present\n');
356
+ writer.stderr('[aw-c4] npm install -g @ghl-ai/aw@latest failed (non-fatal); using existing aw if present\n');
357
357
  }
358
358
 
359
359
  // Step 7 — aw init --silent.
package/commands/init.mjs CHANGED
@@ -23,7 +23,7 @@ import * as p from '@clack/prompts';
23
23
  import * as config from '../config.mjs';
24
24
  import * as fmt from '../fmt.mjs';
25
25
  import { chalk, setSilent } from '../fmt.mjs';
26
- import { linkWorkspace } from '../link.mjs';
26
+ import { linkWorkspace, linkSkills, normalizeSkillTarget } from '../link.mjs';
27
27
  import { generateCommands, copyInstructions, initAwDocs, syncHomeHarnessInstructions } from '../integrate.mjs';
28
28
  import { setupMcp } from '../mcp.mjs';
29
29
  import { applyStoredStartupPreferences, ensureAwRuntimeHook } from '../startup.mjs';
@@ -108,6 +108,35 @@ function scaffoldNamespace(awHome, folderName) {
108
108
  }
109
109
  }
110
110
 
111
+ function getSkillInitSpecs(args) {
112
+ const positional = args._positional || [];
113
+ const mode = positional[0];
114
+ const rawTargets = [];
115
+
116
+ if (mode === 'skill' || mode === 'skills') {
117
+ rawTargets.push(...positional.slice(1));
118
+ }
119
+
120
+ if (args['--skill']) {
121
+ rawTargets.push(args['--skill']);
122
+ }
123
+
124
+ if ((mode === 'skill' || mode === 'skills' || args['--skill']) && rawTargets.length === 0) {
125
+ fmt.cancel(`Missing skill path.\n\n ${chalk.dim('Example:')} ${chalk.bold('aw init skills platform/core/skills/pr-review')}`);
126
+ }
127
+
128
+ if (rawTargets.length === 0) return [];
129
+
130
+ try {
131
+ return [...new Map(rawTargets.map(input => {
132
+ const spec = normalizeSkillTarget(input);
133
+ return [spec.registryPath, spec];
134
+ })).values()];
135
+ } catch (e) {
136
+ fmt.cancel(e.message);
137
+ }
138
+ }
139
+
111
140
  // ── Ensure ~/.aw/.git/info/exclude has the whitelist block ─────────────
112
141
  //
113
142
  // Strategy: only .aw_registry/, .aw_rules/, content/, and .aw_docs/ are
@@ -215,6 +244,14 @@ export async function initCommand(args) {
215
244
  let user = args['--user'] || '';
216
245
  const silent = args['--silent'] === true;
217
246
  const skipIntegrations = args['--no-integrations'] === true;
247
+ const skillSpecs = getSkillInitSpecs(args);
248
+ const skillTargets = skillSpecs.map(spec => spec.registryPath);
249
+ const isSkillInit = skillTargets.length > 0;
250
+ const skillNamespace = skillSpecs[0]?.namespace || null;
251
+
252
+ if (isSkillInit && namespace) {
253
+ fmt.cancel('Use either `aw init skills <path...>` or `aw init --namespace <team/sub-team>`, not both.');
254
+ }
218
255
 
219
256
  // In silent mode, suppress ALL fmt output and show a single spinner.
220
257
  // setSilent(true) makes every fmt.* call a no-op — internal functions
@@ -323,8 +360,26 @@ export async function initCommand(args) {
323
360
  if (isGitNative) {
324
361
  const cfg = config.load(GLOBAL_AW_DIR);
325
362
 
326
- const isNewSubTeam = folderName && cfg && !cfg.include.includes(folderName);
327
- if (isNewSubTeam) {
363
+ const newSkillTargets = isSkillInit && cfg
364
+ ? skillTargets.filter(target => !cfg.include.some(pattern => target === pattern || target.startsWith(pattern + '/')))
365
+ : [];
366
+ const isNewSubTeam = !isSkillInit && folderName && cfg && !cfg.include.includes(folderName);
367
+
368
+ if (isSkillInit) {
369
+ if (newSkillTargets.length > 0) {
370
+ if (!silent) fmt.logStep(`Adding ${chalk.cyan(newSkillTargets.length)} skill${newSkillTargets.length > 1 ? 's' : ''}...`);
371
+ addToSparseCheckout(AW_HOME, [
372
+ ...newSkillTargets.map(target => `.aw_registry/${target}`),
373
+ DOCS_SOURCE_DIR,
374
+ AW_DOCS_DIR,
375
+ RULES_SOURCE_DIR,
376
+ `.aw_registry/AW-PROTOCOL.md`,
377
+ ]);
378
+ for (const target of newSkillTargets) config.addPattern(GLOBAL_AW_DIR, target);
379
+ } else if (!silent) {
380
+ fmt.logStep('Already initialized — syncing selected skills...');
381
+ }
382
+ } else if (isNewSubTeam) {
328
383
  if (!silent) fmt.logStep(`Adding sub-team ${chalk.cyan(folderName)}...`);
329
384
  const newSparsePaths = [`.aw_registry/${folderName}`, DOCS_SOURCE_DIR, AW_DOCS_DIR, RULES_SOURCE_DIR];
330
385
  addToSparseCheckout(AW_HOME, newSparsePaths);
@@ -352,6 +407,7 @@ export async function initCommand(args) {
352
407
 
353
408
  ensureAwGitignore(AW_HOME);
354
409
  const freshCfg = config.load(GLOBAL_AW_DIR);
410
+ const activeNamespace = skillNamespace || freshCfg?.namespace || team;
355
411
  syncRulesTargets(HOME);
356
412
  if (cwd !== HOME) {
357
413
  syncRulesTargets(cwd);
@@ -374,8 +430,8 @@ export async function initCommand(args) {
374
430
  await installAwEcc(cwd, { silent });
375
431
 
376
432
  ensureAwRuntimeHook(HOME);
377
- syncHomeAndProjectInstructions(cwd, freshCfg?.namespace || team);
378
- await setupMcp(HOME, freshCfg?.namespace || team, { silent });
433
+ syncHomeAndProjectInstructions(cwd, activeNamespace);
434
+ await setupMcp(HOME, activeNamespace, { silent });
379
435
  applyStoredStartupPreferences(HOME);
380
436
  const removedLegacyStartupFiles = cwd !== HOME ? removeWorkspaceHookDefaults(cwd) : [];
381
437
  installGlobalHooks();
@@ -409,10 +465,12 @@ export async function initCommand(args) {
409
465
  if (!silent) fmt.logStep('Wiring IDE symlinks...');
410
466
  const projectRegistryDir = cwd !== HOME ? join(cwd, '.aw', REGISTRY_DIR) : null;
411
467
  const awDirForLinks = (projectRegistryDir && existsSync(projectRegistryDir)) ? projectRegistryDir : null;
412
- const symlinks = linkWorkspace(HOME, awDirForLinks, { silent: true });
413
- const commands = generateCommands(HOME, { silent: true });
468
+ const symlinks = isSkillInit
469
+ ? linkSkills(HOME, skillTargets, awDirForLinks, { silent: true, exclusive: true })
470
+ : linkWorkspace(HOME, awDirForLinks, { silent: true });
471
+ const commands = isSkillInit ? 0 : generateCommands(HOME, { silent: true });
414
472
  if (cwd !== HOME) installLocalCommitHook(cwd);
415
- if (!silent) fmt.logStep(`IDE wired — ${chalk.bold(symlinks)} symlinks · ${chalk.bold(commands)} commands`);
473
+ if (!silent) fmt.logStep(`IDE wired — ${chalk.bold(symlinks)} ${isSkillInit ? 'skill symlinks' : 'symlinks'} · ${chalk.bold(commands)} commands`);
416
474
 
417
475
  // Write hook manifest after all hook installation is complete
418
476
  try { writeHookManifest({ eccVersion: AW_ECC_TAG, awVersion: VERSION }); } catch { /* best effort */ }
@@ -420,7 +478,7 @@ export async function initCommand(args) {
420
478
  // Auto-install suggested integrations (Codex, Caveman, Graphify, etc) - unless --no-integrations
421
479
  let installedIntegrations = [];
422
480
  if (!silent && !skipIntegrations && !isNewSubTeam) {
423
- installedIntegrations = await autoInstallIntegrations(freshCfg?.namespace || team, { silent });
481
+ installedIntegrations = await autoInstallIntegrations(activeNamespace, { silent });
424
482
  }
425
483
 
426
484
  if (silent) {
@@ -431,7 +489,7 @@ export async function initCommand(args) {
431
489
  `⟁ ${isNewSubTeam ? `Sub-team ${chalk.cyan(folderName)} added` : 'Up to date'}`,
432
490
  '',
433
491
  ` ${chalk.green('✓')} Registry synced`,
434
- ` ${chalk.green('✓')} IDE refreshed — ${chalk.bold(symlinks)} symlinks · ${chalk.bold(commands)} commands`,
492
+ ` ${chalk.green('✓')} IDE refreshed — ${chalk.bold(symlinks)} ${isSkillInit ? 'skill symlinks' : 'symlinks'} · ${chalk.bold(commands)} commands`,
435
493
  removedLegacyStartupFiles.length > 0
436
494
  ? ` ${chalk.green('✓')} Removed ${removedLegacyStartupFiles.length} legacy repo startup file${removedLegacyStartupFiles.length > 1 ? 's' : ''}`
437
495
  : null,
@@ -465,13 +523,24 @@ export async function initCommand(args) {
465
523
  }
466
524
 
467
525
  // Determine sparse paths
468
- const sparsePaths = [`.aw_registry/platform`, DOCS_SOURCE_DIR, AW_DOCS_DIR, RULES_SOURCE_DIR, `.aw_registry/AW-PROTOCOL.md`, `CODEOWNERS`];
469
- if (folderName) {
526
+ const sparsePaths = isSkillInit
527
+ ? [
528
+ ...skillTargets.map(target => `.aw_registry/${target}`),
529
+ DOCS_SOURCE_DIR,
530
+ AW_DOCS_DIR,
531
+ RULES_SOURCE_DIR,
532
+ `.aw_registry/AW-PROTOCOL.md`,
533
+ `CODEOWNERS`,
534
+ ]
535
+ : [`.aw_registry/platform`, DOCS_SOURCE_DIR, AW_DOCS_DIR, RULES_SOURCE_DIR, `.aw_registry/AW-PROTOCOL.md`, `CODEOWNERS`];
536
+ if (!isSkillInit && folderName) {
470
537
  sparsePaths.push(`.aw_registry/${folderName}`);
471
538
  }
472
539
 
473
540
  fmt.note([
474
- folderName ? `${chalk.dim('namespace:')} ${folderName}` : `${chalk.dim('namespace:')} ${chalk.dim('none')}`,
541
+ isSkillInit
542
+ ? `${chalk.dim('skills:')} ${skillTargets.join(', ')}`
543
+ : (folderName ? `${chalk.dim('namespace:')} ${folderName}` : `${chalk.dim('namespace:')} ${chalk.dim('none')}`),
475
544
  user ? `${chalk.dim('user:')} ${user}` : null,
476
545
  `${chalk.dim('version:')} v${VERSION}`,
477
546
  ].filter(Boolean).join('\n'), 'Config');
@@ -511,8 +580,11 @@ export async function initCommand(args) {
511
580
  }
512
581
 
513
582
  // Create sync config — default to 'platform' when no namespace specified
514
- const cfg = config.create(GLOBAL_AW_DIR, { namespace: team || 'platform', user });
515
- if (folderName) {
583
+ const activeNamespace = skillNamespace || team || 'platform';
584
+ const cfg = config.create(GLOBAL_AW_DIR, { namespace: activeNamespace, user });
585
+ if (isSkillInit) {
586
+ for (const target of skillTargets) config.addPattern(GLOBAL_AW_DIR, target);
587
+ } else if (folderName) {
516
588
  config.addPattern(GLOBAL_AW_DIR, folderName);
517
589
  scaffoldNamespace(AW_HOME, folderName);
518
590
  }
@@ -533,8 +605,8 @@ export async function initCommand(args) {
533
605
 
534
606
  // Parallel batch B: post-ECC setup (instructions and MCP are independent)
535
607
  const [, mcpFiles] = await Promise.all([
536
- Promise.resolve(syncHomeAndProjectInstructions(cwd, team)),
537
- setupMcp(HOME, team, { silent }),
608
+ Promise.resolve(syncHomeAndProjectInstructions(cwd, activeNamespace)),
609
+ setupMcp(HOME, activeNamespace, { silent }),
538
610
  ]);
539
611
  // applyStoredStartupPreferences reads settings written by ECC — keep after batch B
540
612
  applyStoredStartupPreferences(HOME);
@@ -572,11 +644,13 @@ export async function initCommand(args) {
572
644
  const awDirForLinks = (projectRegistryDir && existsSync(projectRegistryDir)) ? projectRegistryDir : null;
573
645
  // Parallel batch C: symlinks + commands are independent
574
646
  if (cwd !== HOME) installLocalCommitHook(cwd);
575
- const [symlinks, commands] = [
576
- linkWorkspace(HOME, awDirForLinks, { silent: true }),
577
- generateCommands(HOME, { silent: true }),
578
- ];
579
- if (!silent) fmt.logStep(`IDE wired — ${chalk.bold(symlinks)} symlinks · ${chalk.bold(commands)} commands`);
647
+ const [symlinks, commands] = isSkillInit
648
+ ? [linkSkills(HOME, skillTargets, awDirForLinks, { silent: true, exclusive: true }), 0]
649
+ : [
650
+ linkWorkspace(HOME, awDirForLinks, { silent: true }),
651
+ generateCommands(HOME, { silent: true }),
652
+ ];
653
+ if (!silent) fmt.logStep(`IDE wired — ${chalk.bold(symlinks)} ${isSkillInit ? 'skill symlinks' : 'symlinks'} · ${chalk.bold(commands)} commands`);
580
654
 
581
655
  // Write hook manifest after all hook installation is complete
582
656
  try { writeHookManifest({ eccVersion: AW_ECC_TAG, awVersion: VERSION }); } catch { /* best effort */ }
@@ -587,7 +661,7 @@ export async function initCommand(args) {
587
661
  // Auto-install suggested integrations (Codex, Caveman, Graphify, etc) - unless --no-integrations
588
662
  let installedIntegrations = [];
589
663
  if (!silent && !skipIntegrations) {
590
- installedIntegrations = await autoInstallIntegrations(team, { silent });
664
+ installedIntegrations = await autoInstallIntegrations(activeNamespace, { silent });
591
665
  }
592
666
 
593
667
  // Offer to update if a newer version is available
package/commands/push.mjs CHANGED
@@ -518,91 +518,6 @@ function writeAwDocsLinkSummary(projectRoot, links, publishConfig) {
518
518
  }
519
519
  }
520
520
 
521
- function normalizeAwDocsRelPath(value) {
522
- if (!value) return null;
523
- let relPath = String(value).replace(/\\/g, '/').replace(/^\.\//, '');
524
- if (relPath.startsWith(`${AW_DOCS_DIR}/`)) relPath = relPath.slice(AW_DOCS_DIR.length + 1);
525
- return relPath.replace(/^\/+/, '');
526
- }
527
-
528
- function featureSlugForAwDocsRelPath(relPath) {
529
- const normalized = normalizeAwDocsRelPath(relPath);
530
- if (!normalized) return null;
531
- const parts = normalized.split('/');
532
- if (parts[0] !== 'features' || !parts[1]) return null;
533
- return parts[1];
534
- }
535
-
536
- function setIfChanged(target, key, value) {
537
- if (target[key] === value) return false;
538
- target[key] = value;
539
- return true;
540
- }
541
-
542
- function inferredMarkdownSourcePath(projectRoot, htmlRelPath) {
543
- const sourceRelPath = htmlRelPath.replace(/\.html$/i, '.md');
544
- if (sourceRelPath === htmlRelPath) return null;
545
- if (!existsSync(join(projectRoot, AW_DOCS_DIR, sourceRelPath))) return null;
546
- return `${AW_DOCS_DIR}/${sourceRelPath}`;
547
- }
548
-
549
- function hydrateAwDocsFeatureStateLinks(projectRoot, links, publishConfig) {
550
- const htmlLinksByFeature = new Map();
551
- for (const link of links) {
552
- if (!/\.html$/i.test(link.relPath)) continue;
553
- const featureSlug = featureSlugForAwDocsRelPath(link.relPath);
554
- if (!featureSlug) continue;
555
- const featureLinks = htmlLinksByFeature.get(featureSlug) || [];
556
- featureLinks.push(link);
557
- htmlLinksByFeature.set(featureSlug, featureLinks);
558
- }
559
-
560
- for (const [featureSlug, featureLinks] of htmlLinksByFeature) {
561
- const statePath = join(projectRoot, AW_DOCS_DIR, 'features', featureSlug, 'state.json');
562
- if (!existsSync(statePath)) continue;
563
-
564
- let state;
565
- try {
566
- state = JSON.parse(readFileSync(statePath, 'utf8'));
567
- } catch (e) {
568
- throw new Error(`Invalid ${AW_DOCS_DIR}/features/${featureSlug}/state.json: ${e.message}`);
569
- }
570
-
571
- const artifacts = Array.isArray(state.html_companion_artifacts)
572
- ? state.html_companion_artifacts
573
- : [];
574
- let changed = !Array.isArray(state.html_companion_artifacts);
575
-
576
- for (const link of featureLinks) {
577
- const htmlPath = `${AW_DOCS_DIR}/${link.relPath}`;
578
- const existing = artifacts.find(artifact => normalizeAwDocsRelPath(artifact?.html_path) === link.relPath);
579
- const artifact = existing || {};
580
- if (!existing) {
581
- const sourcePath = inferredMarkdownSourcePath(projectRoot, link.relPath);
582
- if (sourcePath) artifact.source_path = sourcePath;
583
- artifacts.push(artifact);
584
- changed = true;
585
- }
586
-
587
- changed = setIfChanged(artifact, 'html_path', htmlPath) || changed;
588
- changed = setIfChanged(artifact, 'publish_status', 'published') || changed;
589
- changed = setIfChanged(artifact, 'remote_repo', publishConfig.repo) || changed;
590
- changed = setIfChanged(artifact, 'remote_branch', publishConfig.branch) || changed;
591
- changed = setIfChanged(artifact, 'remote_path', link.publishedPath) || changed;
592
- changed = setIfChanged(artifact, 'platform_docs_path', link.publishedPath) || changed;
593
- changed = setIfChanged(artifact, 'remote_url', link.remoteUrl) || changed;
594
- changed = setIfChanged(artifact, 'teamofone_url', link.remoteUrl) || changed;
595
- changed = setIfChanged(artifact, 'github_url', link.repositoryUrl) || changed;
596
- changed = setIfChanged(artifact, 'repository_url', link.repositoryUrl) || changed;
597
- }
598
-
599
- if (!changed) continue;
600
- state.html_companion_artifacts = artifacts;
601
- state.updated_at = new Date().toISOString();
602
- writeFileSync(statePath, JSON.stringify(state, null, 2) + '\n');
603
- }
604
- }
605
-
606
521
  async function commitAndPushAwDocsRepo(docsRepoDir, { message, branch }) {
607
522
  await execFile('git', ['add', '-A'], { cwd: docsRepoDir, encoding: 'utf8' });
608
523
  await execFile('git', ['commit', '-m', message], {
@@ -697,8 +612,6 @@ async function publishProjectAwDocs(cwd, home, dryRun, scope = null) {
697
612
  return { hasDocs: true, publishedPaths, links };
698
613
  }
699
614
 
700
- hydrateAwDocsFeatureStateLinks(projectRoot, links, publishConfig);
701
-
702
615
  const s = fmt.spinner();
703
616
  s.start(`Publishing ${files.length} AW doc${files.length > 1 ? 's' : ''} to ${publishConfig.repo}...`);
704
617
  try {
package/constants.mjs CHANGED
@@ -33,7 +33,7 @@ export const AW_DOCS_BASE_BRANCH = 'master-sync';
33
33
  export const AW_DOCS_SEED_BRANCH = process.env.AW_DOCS_SEED_BRANCH || 'scaffold';
34
34
  export const AW_DOCS_PUBLISH_DIR = 'aw_docs';
35
35
  export const AW_DOCS_PUBLIC_BASE_URL = process.env.AW_DOCS_PUBLIC_BASE_URL || `https://github.com/${AW_DOCS_REPO}/blob/${AW_DOCS_BASE_BRANCH}`;
36
- export const AW_DOCS_TEAMOFONE_ORIGIN = process.env.AW_DOCS_TEAMOFONE_ORIGIN || 'https://teamofone.servers.stg.msgsndr.net';
36
+ export const AW_DOCS_TEAMOFONE_ORIGIN = process.env.AW_DOCS_TEAMOFONE_ORIGIN || 'https://teamofone.msgsndr.net';
37
37
  export const AW_DOCS_TEAMOFONE_BASE_URL = process.env.AW_DOCS_TEAMOFONE_BASE_URL || `${AW_DOCS_TEAMOFONE_ORIGIN}/too/docs/GoHighLevel/ghl-aw-docs`;
38
38
 
39
39
  export function defaultAwDocsGithubDocsConfig() {
package/integrate.mjs CHANGED
@@ -649,9 +649,6 @@ No active runs. Use \`/aw:<team>-<command>\` to start a workflow.
649
649
  // config.json — registry paths, sync settings
650
650
  const configPath = join(awDocsDir, 'config.json');
651
651
  const defaultConfig = {
652
- docs: {
653
- outputMode: 'dual',
654
- },
655
652
  sync: {
656
653
  eager: true,
657
654
  batch_threshold: 10,
@@ -671,10 +668,6 @@ No active runs. Use \`/aw:<team>-<command>\` to start a workflow.
671
668
  const existing = JSON.parse(readFileSync(configPath, 'utf8'));
672
669
  nextConfig = {
673
670
  ...existing,
674
- docs: {
675
- ...defaultConfig.docs,
676
- ...(existing.docs || {}),
677
- },
678
671
  sync: {
679
672
  ...defaultConfig.sync,
680
673
  ...(existing.sync || {}),
package/link.mjs CHANGED
@@ -16,6 +16,11 @@ const IDE_DIRS = ['.claude', '.cursor', '.codex'];
16
16
  const FILE_TYPES = ['agents'];
17
17
  const ALL_KNOWN_TYPES = new Set([...FILE_TYPES, 'skills', 'commands', 'evals', 'references', 'docs']);
18
18
 
19
+ function realHomeDir() {
20
+ const rawHome = homedir();
21
+ try { return realpathSync(rawHome); } catch { return rawHome; }
22
+ }
23
+
19
24
  /**
20
25
  * List namespace directories inside .aw_registry/ (skip dotfiles).
21
26
  */
@@ -77,7 +82,7 @@ function cleanIdeSymlinks(cwd) {
77
82
  cleanSymlinksRecursive(ideDir);
78
83
  }
79
84
  // Also clean .agents/skills/ (global only — Codex reads from ~/.agents/skills/)
80
- const HOME = homedir();
85
+ const HOME = realHomeDir();
81
86
  if (cwd === HOME) {
82
87
  const agentsSkillsDir = join(cwd, '.agents', 'skills');
83
88
  if (existsSync(agentsSkillsDir)) cleanSymlinksRecursive(agentsSkillsDir);
@@ -120,6 +125,117 @@ function flatName(ns, name) {
120
125
  return `${ns}-${name}`;
121
126
  }
122
127
 
128
+ function stripRegistryPrefix(input) {
129
+ return String(input || '')
130
+ .trim()
131
+ .replace(/\\/g, '/')
132
+ .replace(/\/SKILL\.md$/i, '')
133
+ .replace(/^.*?\.aw_registry\//, '')
134
+ .replace(/^\.\/+/, '')
135
+ .replace(/\/+$/, '');
136
+ }
137
+
138
+ export function normalizeSkillTarget(input) {
139
+ const normalized = stripRegistryPrefix(input);
140
+ const parts = normalized.split('/').filter(Boolean);
141
+ const skillIndex = parts.indexOf('skills');
142
+
143
+ if (skillIndex < 1 || skillIndex === parts.length - 1) {
144
+ throw new Error(`Skill target must look like <team>/<path>/skills/<name>: ${input}`);
145
+ }
146
+
147
+ if (parts.length !== skillIndex + 2) {
148
+ throw new Error(`Skill target must point at a skill folder, not a nested file: ${input}`);
149
+ }
150
+
151
+ const namespace = parts[0];
152
+ const segments = parts.slice(1, skillIndex);
153
+ const skill = parts[skillIndex + 1];
154
+ const registryPath = [namespace, ...segments, 'skills', skill].join('/');
155
+ const flat = [namespace, ...segments, skill].join('-');
156
+ return { namespace, segments, skill, registryPath, flat };
157
+ }
158
+
159
+ function cleanSkillSymlinks(cwd) {
160
+ for (const ide of IDE_DIRS) {
161
+ const skillsDir = join(cwd, ide, 'skills');
162
+ if (existsSync(skillsDir)) cleanSymlinksRecursive(skillsDir);
163
+ }
164
+
165
+ if (cwd === realHomeDir()) {
166
+ const agentsSkillsDir = join(cwd, '.agents', 'skills');
167
+ if (existsSync(agentsSkillsDir)) cleanSymlinksRecursive(agentsSkillsDir);
168
+ }
169
+ }
170
+
171
+ function linkOneSkill(cwd, awDir, target) {
172
+ const skillDir = join(awDir, target.namespace, ...target.segments, 'skills', target.skill);
173
+ if (!existsSync(skillDir) || !lstatSync(skillDir).isDirectory()) {
174
+ throw new Error(`Skill not found in registry: ${target.registryPath}`);
175
+ }
176
+
177
+ let created = 0;
178
+ for (const ide of IDE_DIRS) {
179
+ const linkDir = join(cwd, ide, 'skills');
180
+ mkdirSync(linkDir, { recursive: true });
181
+ const linkPath = join(linkDir, target.flat);
182
+ const relTarget = relative(linkDir, skillDir);
183
+ forceSymlink(relTarget, linkPath);
184
+ created++;
185
+ }
186
+
187
+ if (cwd === realHomeDir()) {
188
+ const agentsSkillsDir = join(cwd, '.agents', 'skills');
189
+ mkdirSync(agentsSkillsDir, { recursive: true });
190
+ const linkPath = join(agentsSkillsDir, target.flat);
191
+ const relTarget = relative(agentsSkillsDir, skillDir);
192
+ forceSymlink(relTarget, linkPath);
193
+ created++;
194
+ }
195
+
196
+ return created;
197
+ }
198
+
199
+ /**
200
+ * Create/refresh symlinks for specific skills only.
201
+ *
202
+ * Unlike linkWorkspace(), this does not walk every namespace. It can be used
203
+ * by lean init flows to avoid exposing every registry skill to IDE runtimes.
204
+ */
205
+ export function linkSkills(cwd, skillTargets, awDirOverride = null, { silent = false, exclusive = false } = {}) {
206
+ try { cwd = realpathSync(cwd); } catch { /* use as-is */ }
207
+
208
+ const GLOBAL_AW_DIR = join(realHomeDir(), '.aw_registry');
209
+ let awDir = awDirOverride || getLocalRegistryDir(cwd, GLOBAL_AW_DIR);
210
+ try { awDir = realpathSync(awDir); } catch { /* use as-is if it doesn't exist */ }
211
+ if (!existsSync(awDir)) return 0;
212
+
213
+ const targets = [...new Map(skillTargets.map(input => {
214
+ const target = normalizeSkillTarget(input);
215
+ return [target.registryPath, target];
216
+ })).values()];
217
+
218
+ for (const target of targets) {
219
+ const skillDir = join(awDir, target.namespace, ...target.segments, 'skills', target.skill);
220
+ if (!existsSync(skillDir) || !lstatSync(skillDir).isDirectory()) {
221
+ throw new Error(`Skill not found in registry: ${target.registryPath}`);
222
+ }
223
+ }
224
+
225
+ if (exclusive) cleanSkillSymlinks(cwd);
226
+
227
+ let created = 0;
228
+ for (const target of targets) {
229
+ created += linkOneSkill(cwd, awDir, target);
230
+ }
231
+
232
+ if (created > 0 && !silent) {
233
+ fmt.logSuccess(`Linked ${created} skill symlink${created > 1 ? 's' : ''}`);
234
+ }
235
+
236
+ return created;
237
+ }
238
+
123
239
  /**
124
240
  * Create/refresh all IDE symlinks.
125
241
  *
@@ -140,7 +256,7 @@ export function linkWorkspace(cwd, awDirOverride = null, { silent = false } = {}
140
256
  // where $HOME may be /var/... but process.cwd() resolves to /private/var/...
141
257
  try { cwd = realpathSync(cwd); } catch { /* use as-is */ }
142
258
 
143
- const GLOBAL_AW_DIR = join(homedir(), '.aw_registry');
259
+ const GLOBAL_AW_DIR = join(realHomeDir(), '.aw_registry');
144
260
  let awDir = awDirOverride || getLocalRegistryDir(cwd, GLOBAL_AW_DIR);
145
261
  try { awDir = realpathSync(awDir); } catch { /* use as-is if it doesn't exist */ }
146
262
  if (!existsSync(awDir)) return 0;
@@ -246,7 +362,7 @@ export function linkWorkspace(cwd, awDirOverride = null, { silent = false } = {}
246
362
  }
247
363
 
248
364
  // Codex per-skill symlinks: ~/.agents/skills/<name> (global only)
249
- if (cwd === homedir()) {
365
+ if (cwd === realHomeDir()) {
250
366
  const agentsSkillsDir = join(cwd, '.agents/skills');
251
367
  for (const ns of namespaces) {
252
368
  for (const { typeDirPath: skillsDir, segments } of findNestedTypeDirs(join(awDir, ns), 'skills')) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghl-ai/aw",
3
- "version": "0.1.49-beta.0",
3
+ "version": "0.1.50-beta.0",
4
4
  "description": "Agentic Workspace CLI — pull, push & manage agents, skills and commands from the registry",
5
5
  "type": "module",
6
6
  "bin": {