@ghl-ai/aw 0.1.25-beta.1 → 0.1.25-beta.11

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/cli.mjs CHANGED
@@ -71,8 +71,9 @@ function printHelp() {
71
71
  const sec = (title) => `\n ${chalk.bold.underline(title)}`;
72
72
  const help = [
73
73
  sec('Setup'),
74
- cmd('aw init --namespace <team>', 'Initialize workspace (required)'),
74
+ cmd('aw init --namespace <team/sub-team>', 'Initialize workspace (required)'),
75
75
  ` ${chalk.dim('Teams: revex, mobile, commerce, leadgen, crm, marketplace, ai')}`,
76
+ ` ${chalk.dim('Example: aw init --namespace revex/courses')}`,
76
77
 
77
78
  sec('Download'),
78
79
  cmd('aw pull', 'Re-pull all synced paths (like git pull)'),
@@ -99,7 +100,7 @@ function printHelp() {
99
100
  sec('Examples'),
100
101
  '',
101
102
  ` ${chalk.dim('# Pull content from registry using path')}`,
102
- cmd('aw pull ghl', 'All shared platform agents & skills'),
103
+ cmd('aw pull platform', 'All shared platform agents & skills'),
103
104
  cmd('aw pull <team>', 'Everything from a team namespace'),
104
105
  cmd('aw pull <team>/agents', 'All agents from a team'),
105
106
  cmd('aw pull <team>/agents/<name>', 'One specific agent'),
package/commands/init.mjs CHANGED
@@ -13,7 +13,7 @@ import { readFileSync } from 'node:fs';
13
13
  import * as config from '../config.mjs';
14
14
  import * as fmt from '../fmt.mjs';
15
15
  import { chalk } from '../fmt.mjs';
16
- import { pullCommand } from './pull.mjs';
16
+ import { pullCommand, pullAsync } from './pull.mjs';
17
17
  import { linkWorkspace } from '../link.mjs';
18
18
  import { generateCommands, copyInstructions, initAwDocs } from '../integrate.mjs';
19
19
  import { setupMcp } from '../mcp.mjs';
@@ -112,6 +112,24 @@ function saveManifest(data) {
112
112
  }
113
113
 
114
114
 
115
+ function printPullSummary(pattern, actions) {
116
+ for (const type of ['agents', 'skills', 'commands', 'evals']) {
117
+ const typeActions = actions.filter(a => a.type === type);
118
+ if (typeActions.length === 0) continue;
119
+
120
+ const counts = { ADD: 0, UPDATE: 0, CONFLICT: 0 };
121
+ for (const a of typeActions) counts[a.action] = (counts[a.action] || 0) + 1;
122
+
123
+ const parts = [];
124
+ if (counts.ADD > 0) parts.push(chalk.green(`${counts.ADD} new`));
125
+ if (counts.UPDATE > 0) parts.push(chalk.cyan(`${counts.UPDATE} updated`));
126
+ if (counts.CONFLICT > 0) parts.push(chalk.red(`${counts.CONFLICT} conflict`));
127
+ const detail = parts.length > 0 ? ` (${parts.join(', ')})` : '';
128
+
129
+ fmt.logSuccess(`${chalk.cyan(pattern)}: ${typeActions.length} ${type}${detail}`);
130
+ }
131
+ }
132
+
115
133
  const ALLOWED_NAMESPACES = ['revex', 'mobile', 'commerce', 'leadgen', 'crm', 'marketplace', 'ai'];
116
134
 
117
135
  export async function initCommand(args) {
@@ -128,45 +146,102 @@ export async function initCommand(args) {
128
146
  fmt.cancel([
129
147
  `Missing required ${chalk.bold('--namespace')} flag`,
130
148
  '',
131
- ` ${chalk.dim('Usage:')} aw init --namespace <team>`,
149
+ ` ${chalk.dim('Usage:')} aw init --namespace <team/sub-team>`,
132
150
  ` ${chalk.dim('Teams:')} ${list}`,
133
151
  '',
134
- ` ${chalk.dim('Example:')} ${chalk.bold('aw init --namespace commerce')}`,
152
+ ` ${chalk.dim('Example:')} ${chalk.bold('aw init --namespace commerce/payments')}`,
135
153
  ].join('\n'));
136
154
  }
137
155
 
138
- if (namespace && !ALLOWED_NAMESPACES.includes(namespace)) {
156
+ // Parse team/sub-team
157
+ const nsParts = namespace ? namespace.split('/') : [];
158
+ const team = nsParts[0] || null;
159
+ const subTeam = nsParts[1] || null;
160
+ const teamNS = subTeam ? `${team}-${subTeam}` : team; // for $TEAM_NS replacement
161
+ const folderName = subTeam ? `${team}/${subTeam}` : team; // for .aw_registry/ path
162
+
163
+ if (team && !ALLOWED_NAMESPACES.includes(team)) {
139
164
  const list = ALLOWED_NAMESPACES.map(n => chalk.cyan(n)).join(', ');
140
165
  fmt.cancel([
141
- `Unknown namespace ${chalk.red(namespace)}`,
166
+ `Unknown team ${chalk.red(team)}`,
142
167
  '',
143
168
  ` ${chalk.dim('Allowed:')} ${list}`,
144
169
  '',
145
- ` ${chalk.dim('Example:')} ${chalk.bold('aw init --namespace commerce')}`,
170
+ ` ${chalk.dim('Example:')} ${chalk.bold('aw init --namespace commerce/payments')}`,
146
171
  ].join('\n'));
147
172
  }
148
173
 
149
- if (namespace && !/^[a-z][a-z0-9-]{1,38}[a-z0-9]$/.test(namespace)) {
150
- fmt.cancel(`Invalid namespace '${namespace}' — must match: ^[a-z][a-z0-9-]{1,38}[a-z0-9]$`);
174
+ if (team && !subTeam) {
175
+ fmt.cancel([
176
+ `Missing sub-team in ${chalk.red(namespace)}`,
177
+ '',
178
+ ` ${chalk.dim('Format:')} --namespace <team>/<sub-team>`,
179
+ '',
180
+ ` ${chalk.dim('Example:')} ${chalk.bold(`aw init --namespace ${team}/courses`)}`,
181
+ ].join('\n'));
151
182
  }
152
183
 
153
- const isExisting = config.exists(GLOBAL_AW_DIR);
184
+ const SLUG_RE = /^[a-z][a-z0-9-]{1,38}[a-z0-9]$/;
185
+ if (team && !SLUG_RE.test(team)) {
186
+ fmt.cancel(`Invalid team '${team}' — must match: ${SLUG_RE}`);
187
+ }
188
+ if (subTeam && !SLUG_RE.test(subTeam)) {
189
+ fmt.cancel(`Invalid sub-team '${subTeam}' — must match: ${SLUG_RE}`);
190
+ }
191
+
192
+ const hasConfig = config.exists(GLOBAL_AW_DIR);
193
+ const hasPlatform = existsSync(join(GLOBAL_AW_DIR, 'platform'));
194
+ const isExisting = hasConfig && hasPlatform;
154
195
  const cwd = process.cwd();
155
196
 
156
197
  // ── Fast path: already initialized → just pull + link ─────────────────
157
198
 
158
199
  if (isExisting) {
159
- if (!silent) fmt.logStep('Already initialized — syncing...');
200
+ const cfg = config.load(GLOBAL_AW_DIR);
201
+
202
+ // Add new sub-team if not already tracked
203
+ const isNewSubTeam = folderName && cfg && !cfg.include.includes(folderName);
204
+ if (isNewSubTeam) {
205
+ if (!silent) fmt.logStep(`Adding sub-team ${chalk.cyan(folderName)}...`);
206
+ await pullAsync({
207
+ ...args,
208
+ _positional: ['[template]'],
209
+ _renameNamespace: folderName,
210
+ _teamNS: teamNS,
211
+ _workspaceDir: GLOBAL_AW_DIR,
212
+ _skipIntegrate: true,
213
+ });
214
+ config.addPattern(GLOBAL_AW_DIR, folderName);
215
+ } else {
216
+ if (!silent) fmt.logStep('Already initialized — syncing...');
217
+ }
160
218
 
161
- // Pull latest
162
- pullCommand({ ...args, _positional: [], _workspaceDir: GLOBAL_AW_DIR, '--silent': silent });
219
+ // Pull latest (parallel)
220
+ // cfg.include has the renamed namespace (e.g. 'revex/courses'), but the repo
221
+ // only has 'registry/[template]/' — remap non-platform entries back.
222
+ const freshCfg = config.load(GLOBAL_AW_DIR);
223
+ if (freshCfg && freshCfg.include.length > 0) {
224
+ const pullJobs = freshCfg.include.map(p => {
225
+ const isTeamNs = p !== 'platform';
226
+ const derivedTeamNS = isTeamNs ? p.replace(/\//g, '-') : undefined;
227
+ return pullAsync({
228
+ ...args,
229
+ _positional: [isTeamNs ? '[template]' : p],
230
+ _workspaceDir: GLOBAL_AW_DIR,
231
+ _skipIntegrate: true,
232
+ _renameNamespace: isTeamNs ? p : undefined,
233
+ _teamNS: derivedTeamNS,
234
+ });
235
+ });
236
+ await Promise.all(pullJobs);
237
+ }
163
238
 
164
239
  // Re-link IDE dirs (idempotent)
165
240
  linkWorkspace(HOME);
166
241
  generateCommands(HOME);
167
- copyInstructions(HOME, null, namespace) || [];
242
+ copyInstructions(HOME, null, freshCfg?.namespace || team) || [];
168
243
  initAwDocs(HOME);
169
- setupMcp(HOME, namespace) || [];
244
+ setupMcp(HOME, freshCfg?.namespace || team) || [];
170
245
 
171
246
  // Link current project if needed
172
247
  if (cwd !== HOME && !existsSync(join(cwd, '.aw_registry'))) {
@@ -178,7 +253,7 @@ export async function initCommand(args) {
178
253
 
179
254
  if (!silent) {
180
255
  fmt.outro([
181
- 'Sync complete',
256
+ isNewSubTeam ? `Sub-team ${chalk.cyan(folderName)} added` : 'Sync complete',
182
257
  '',
183
258
  ` ${chalk.green('✓')} Registry updated`,
184
259
  ` ${chalk.green('✓')} IDE integration refreshed`,
@@ -200,36 +275,53 @@ export async function initCommand(args) {
200
275
  // Step 1: Create global source of truth
201
276
  mkdirSync(GLOBAL_AW_DIR, { recursive: true });
202
277
 
203
- const cfg = config.create(GLOBAL_AW_DIR, { namespace, user });
278
+ const cfg = config.create(GLOBAL_AW_DIR, { namespace: team, user });
204
279
 
205
280
  fmt.note([
206
281
  `${chalk.dim('source:')} ~/.aw_registry/`,
207
- namespace ? `${chalk.dim('namespace:')} ${cfg.namespace}` : `${chalk.dim('namespace:')} ${chalk.dim('none')}`,
282
+ folderName ? `${chalk.dim('namespace:')} ${folderName}` : `${chalk.dim('namespace:')} ${chalk.dim('none')}`,
208
283
  user ? `${chalk.dim('user:')} ${cfg.user}` : null,
209
284
  `${chalk.dim('version:')} v${VERSION}`,
210
285
  ].filter(Boolean).join('\n'), 'Config created');
211
286
 
212
- // Step 2: Pull registry content
213
- pullCommand({ ...args, _positional: ['ghl'], _workspaceDir: GLOBAL_AW_DIR });
287
+ // Step 2: Pull registry content (parallel)
288
+ const s = fmt.spinner();
289
+ const pullTargets = folderName ? `platform + ${folderName}` : 'platform';
290
+ s.start(`Pulling ${pullTargets}...`);
291
+
292
+ const pullJobs = [
293
+ pullAsync({ ...args, _positional: ['platform'], _workspaceDir: GLOBAL_AW_DIR, _skipIntegrate: true }),
294
+ ];
295
+ if (folderName) {
296
+ pullJobs.push(
297
+ pullAsync({ ...args, _positional: ['[template]'], _renameNamespace: folderName, _teamNS: teamNS, _workspaceDir: GLOBAL_AW_DIR, _skipIntegrate: true }),
298
+ );
299
+ }
214
300
 
215
- if (namespace) {
216
- pullCommand({ ...args, _positional: ['[template]'], _renameNamespace: namespace, _workspaceDir: GLOBAL_AW_DIR });
301
+ let pullResults;
302
+ try {
303
+ pullResults = await Promise.all(pullJobs);
304
+ s.stop(`Pulled ${pullTargets}`);
305
+ } catch (e) {
306
+ s.stop(chalk.red('Pull failed'));
307
+ fmt.cancel(e.message);
308
+ }
309
+
310
+ for (const { pattern, actions } of pullResults) {
311
+ printPullSummary(pattern, actions);
217
312
  }
218
313
 
219
- // Step 3: Link to global IDE dirs
314
+ // Step 3: Link IDE dirs + setup tasks
315
+ fmt.logStep('Linking IDE symlinks...');
220
316
  linkWorkspace(HOME);
221
317
  generateCommands(HOME);
222
- const instructionFiles = copyInstructions(HOME, null, namespace) || [];
318
+ const instructionFiles = copyInstructions(HOME, null, team) || [];
223
319
  initAwDocs(HOME);
224
- const mcpFiles = setupMcp(HOME, namespace) || [];
225
-
226
- // Step 4: Git template hook (omnipresence)
320
+ const mcpFiles = setupMcp(HOME, team) || [];
227
321
  const gitTemplateInstalled = installGitTemplate();
228
-
229
- // Step 5: IDE auto-init tasks
230
322
  installIdeTasks();
231
323
 
232
- // Step 6: Symlink in current directory if it's a git repo
324
+ // Step 4: Symlink in current directory if it's a git repo
233
325
  if (cwd !== HOME && !existsSync(join(cwd, '.aw_registry'))) {
234
326
  try {
235
327
  symlinkSync(GLOBAL_AW_DIR, join(cwd, '.aw_registry'));
@@ -237,7 +329,7 @@ export async function initCommand(args) {
237
329
  } catch { /* best effort */ }
238
330
  }
239
331
 
240
- // Step 7: Write manifest for nuke cleanup
332
+ // Step 5: Write manifest for nuke cleanup
241
333
  const manifest = {
242
334
  version: 1,
243
335
  installedAt: new Date().toISOString(),
package/commands/pull.mjs CHANGED
@@ -7,7 +7,7 @@ import { execSync } from 'node:child_process';
7
7
  import * as config from '../config.mjs';
8
8
  import * as fmt from '../fmt.mjs';
9
9
  import { chalk } from '../fmt.mjs';
10
- import { sparseCheckout, cleanup, includeToSparsePaths } from '../git.mjs';
10
+ import { sparseCheckout, sparseCheckoutAsync, cleanup, includeToSparsePaths } from '../git.mjs';
11
11
  import { walkRegistryTree } from '../registry.mjs';
12
12
  import { matchesAny } from '../glob.mjs';
13
13
  import { computePlan } from '../plan.mjs';
@@ -17,7 +17,15 @@ import { resolveInput } from '../paths.mjs';
17
17
  import { linkWorkspace } from '../link.mjs';
18
18
  import { generateCommands, copyInstructions } from '../integrate.mjs';
19
19
 
20
- export function pullCommand(args) {
20
+ // Platform-level types to skip (CLI meta-docs, not useful as agent content)
21
+ const SKIP_PLATFORM_TYPES = new Set(['commands']);
22
+
23
+ function filterActions(actions, pattern) {
24
+ if (pattern !== 'platform') return actions;
25
+ return actions.filter(a => !SKIP_PLATFORM_TYPES.has(a.type));
26
+ }
27
+
28
+ export async function pullCommand(args) {
21
29
  const input = args._positional?.[0] || '';
22
30
  const cwd = process.cwd();
23
31
  const GLOBAL_AW_DIR = join(homedir(), '.aw_registry');
@@ -27,6 +35,7 @@ export function pullCommand(args) {
27
35
  const verbose = args['-v'] === true || args['--verbose'] === true;
28
36
  const silent = args['--silent'] === true || args._silent === true;
29
37
  const renameNamespace = args._renameNamespace || null;
38
+ const teamNSOverride = args._teamNS || null;
30
39
 
31
40
  // Silent mode: wrap fmt to suppress all output and exit cleanly on errors
32
41
  const log = {
@@ -41,7 +50,7 @@ export function pullCommand(args) {
41
50
  spinner: silent ? () => ({ start: () => {}, stop: () => {} }) : fmt.spinner,
42
51
  };
43
52
 
44
- // No args = re-pull everything in sync config
53
+ // No args = re-pull everything in sync config (parallel)
45
54
  if (!input) {
46
55
  const cfg = config.load(workspaceDir);
47
56
  if (!cfg) log.cancel('No .sync-config.json found. Run: aw init');
@@ -49,9 +58,19 @@ export function pullCommand(args) {
49
58
  log.cancel('Nothing to pull. Add paths first:\n\n aw pull <path>');
50
59
  }
51
60
  log.logInfo(`Pulling ${chalk.cyan(cfg.include.length)} synced path${cfg.include.length > 1 ? 's' : ''}...`);
52
- for (const p of cfg.include) {
53
- pullCommand({ ...args, _positional: [p], _skipIntegrate: true });
54
- }
61
+ const pullJobs = cfg.include.map(p => {
62
+ const isTeamNs = p !== 'platform';
63
+ const derivedTeamNS = isTeamNs ? p.replace(/\//g, '-') : undefined;
64
+ return pullAsync({
65
+ ...args,
66
+ _positional: [isTeamNs ? '[template]' : p],
67
+ _workspaceDir: workspaceDir,
68
+ _skipIntegrate: true,
69
+ _renameNamespace: isTeamNs ? p : undefined,
70
+ _teamNS: derivedTeamNS,
71
+ });
72
+ });
73
+ await Promise.all(pullJobs);
55
74
  // Post-pull IDE integration (once after all paths pulled)
56
75
  linkWorkspace(cwd);
57
76
  generateCommands(cwd);
@@ -145,9 +164,10 @@ export function pullCommand(args) {
145
164
  }
146
165
 
147
166
  // Compute plan
148
- const { actions } = computePlan(registryDirs, workspaceDir, [pattern], {
167
+ const { actions: rawActions } = computePlan(registryDirs, workspaceDir, [pattern], {
149
168
  skipOrphans: true,
150
169
  });
170
+ const actions = filterActions(rawActions, pattern);
151
171
 
152
172
  if (dryRun) {
153
173
  printDryRun(actions, verbose);
@@ -157,7 +177,7 @@ export function pullCommand(args) {
157
177
  // Apply
158
178
  const s2 = log.spinner();
159
179
  s2.start('Applying changes...');
160
- const conflictCount = applyActions(actions, { teamNS: renameNamespace || undefined });
180
+ const conflictCount = applyActions(actions, { teamNS: teamNSOverride || renameNamespace || undefined });
161
181
  updateManifest(workspaceDir, actions, cfg.namespace);
162
182
  s2.stop('Changes applied');
163
183
 
@@ -191,6 +211,84 @@ export function pullCommand(args) {
191
211
  }
192
212
  }
193
213
 
214
+ /**
215
+ * Async pull for parallel use by init. Runs git fetch asynchronously,
216
+ * then applies changes synchronously. Returns a summary object instead
217
+ * of printing directly — the caller controls output.
218
+ */
219
+ export async function pullAsync(args) {
220
+ const input = args._positional?.[0] || '';
221
+ const workspaceDir = args._workspaceDir;
222
+ const renameNamespace = args._renameNamespace || null;
223
+ const teamNSOverride = args._teamNS || null;
224
+
225
+ const resolved = resolveInput(input, workspaceDir);
226
+ let pattern = resolved.registryPath;
227
+ if (!pattern) throw new Error(`Could not resolve "${input}" to a registry path`);
228
+
229
+ if (!existsSync(workspaceDir)) mkdirSync(workspaceDir, { recursive: true });
230
+
231
+ const cfg = config.load(workspaceDir);
232
+ if (!cfg) throw new Error('No .sync-config.json found');
233
+
234
+ const sparsePaths = includeToSparsePaths([pattern]);
235
+ const tempDir = await sparseCheckoutAsync(cfg.repo, sparsePaths);
236
+
237
+ try {
238
+ const registryDirs = [];
239
+ const regBase = join(tempDir, 'registry');
240
+
241
+ if (existsSync(regBase)) {
242
+ for (const name of listDirs(regBase)) {
243
+ registryDirs.push({ name, path: join(regBase, name) });
244
+ }
245
+ }
246
+
247
+ if (registryDirs.length === 0) {
248
+ throw new Error(`Nothing found in registry for ${pattern}`);
249
+ }
250
+
251
+ if (renameNamespace) {
252
+ for (const rd of registryDirs) {
253
+ if (rd.name === pattern) rd.name = renameNamespace;
254
+ }
255
+ pattern = renameNamespace;
256
+ }
257
+
258
+ let hasMatch = false;
259
+ for (const { name, path } of registryDirs) {
260
+ const entries = walkRegistryTree(path, name);
261
+ if (entries.some(e => matchesAny(e.registryPath, [pattern]))) {
262
+ hasMatch = true;
263
+ break;
264
+ }
265
+ }
266
+
267
+ if (!hasMatch) {
268
+ throw new Error(`Nothing found in registry for ${pattern}`);
269
+ }
270
+
271
+ if (!cfg.include.includes(pattern)) {
272
+ config.addPattern(workspaceDir, pattern);
273
+ }
274
+
275
+ const { actions: rawActions } = computePlan(registryDirs, workspaceDir, [pattern], { skipOrphans: true });
276
+ const actions = filterActions(rawActions, pattern);
277
+ const conflictCount = applyActions(actions, { teamNS: teamNSOverride || renameNamespace || undefined });
278
+ updateManifest(workspaceDir, actions, cfg.namespace);
279
+
280
+ const ROOT_REGISTRY_FILES = ['AW-PROTOCOL.md'];
281
+ for (const fname of ROOT_REGISTRY_FILES) {
282
+ const src = join(regBase, fname);
283
+ if (existsSync(src)) copyFileSync(src, join(workspaceDir, fname));
284
+ }
285
+
286
+ return { pattern, actions, conflictCount };
287
+ } finally {
288
+ cleanup(tempDir);
289
+ }
290
+ }
291
+
194
292
  function listDirs(dir) {
195
293
  return readdirSync(dir, { withFileTypes: true })
196
294
  .filter(d => d.isDirectory() && !d.name.startsWith('.'))
package/commands/push.mjs CHANGED
@@ -92,8 +92,8 @@ export function pushCommand(args) {
92
92
  const namespacePath = namespaceParts.join('/');
93
93
  const topNamespace = namespaceParts[0];
94
94
 
95
- if (topNamespace === 'ghl') {
96
- fmt.cancel("Cannot push to 'ghl' namespace — it is the shared platform layer");
95
+ if (topNamespace === 'platform') {
96
+ fmt.cancel("Cannot push to 'platform' namespace — it is the shared platform layer");
97
97
  }
98
98
 
99
99
  const isDir = statSync(absPath).isDirectory();
package/git.mjs CHANGED
@@ -1,13 +1,16 @@
1
1
  // git.mjs — Git sparse checkout helpers. Zero dependencies.
2
2
 
3
- import { execSync } from 'node:child_process';
3
+ import { execSync, exec as execCb } from 'node:child_process';
4
4
  import { mkdtempSync, existsSync } from 'node:fs';
5
5
  import { join } from 'node:path';
6
6
  import { tmpdir } from 'node:os';
7
+ import { promisify } from 'node:util';
7
8
  import { REGISTRY_BASE_BRANCH } from './constants.mjs';
8
9
 
10
+ const exec = promisify(execCb);
11
+
9
12
  /**
10
- * Sparse-checkout registry paths from GitHub.
13
+ * Sparse-checkout registry paths from GitHub (sync).
11
14
  * Returns the temp directory path containing the checkout.
12
15
  */
13
16
  export function sparseCheckout(repo, paths) {
@@ -35,6 +38,31 @@ export function sparseCheckout(repo, paths) {
35
38
  return tempDir;
36
39
  }
37
40
 
41
+ /**
42
+ * Sparse-checkout registry paths from GitHub (async).
43
+ * Same as sparseCheckout but non-blocking — can run in parallel.
44
+ */
45
+ export async function sparseCheckoutAsync(repo, paths) {
46
+ const tempDir = mkdtempSync(join(tmpdir(), 'aw-'));
47
+
48
+ const repoUrl = repo.startsWith('http') ? repo : `https://github.com/${repo}.git`;
49
+ try {
50
+ await exec(`git clone --filter=blob:none --no-checkout "${repoUrl}" "${tempDir}"`);
51
+ } catch (e) {
52
+ throw new Error(`Failed to clone ${repo}. Check your git credentials and repo access.`);
53
+ }
54
+
55
+ try {
56
+ await exec('git sparse-checkout init --cone', { cwd: tempDir });
57
+ await exec(`git sparse-checkout set --skip-checks ${paths.map(p => `"${p}"`).join(' ')}`, { cwd: tempDir });
58
+ await exec(`git checkout ${REGISTRY_BASE_BRANCH}`, { cwd: tempDir });
59
+ } catch (e) {
60
+ throw new Error(`Failed sparse checkout: ${e.message}`);
61
+ }
62
+
63
+ return tempDir;
64
+ }
65
+
38
66
  /**
39
67
  * Clean up temp directory.
40
68
  */
package/link.mjs CHANGED
@@ -3,6 +3,7 @@
3
3
  import { existsSync, lstatSync, mkdirSync, readdirSync, unlinkSync } from 'node:fs';
4
4
  import { join, relative } from 'node:path';
5
5
  import { execSync } from 'node:child_process';
6
+ import { homedir } from 'node:os';
6
7
  import * as fmt from './fmt.mjs';
7
8
 
8
9
  const IDE_DIRS = ['.claude', '.cursor', '.codex'];
@@ -66,9 +67,12 @@ function cleanIdeSymlinks(cwd) {
66
67
  if (!existsSync(ideDir)) continue;
67
68
  cleanSymlinksRecursive(ideDir);
68
69
  }
69
- // Also clean .agents/skills/
70
- const agentsSkillsDir = join(cwd, '.agents', 'skills');
71
- if (existsSync(agentsSkillsDir)) cleanSymlinksRecursive(agentsSkillsDir);
70
+ // Also clean .agents/skills/ (global only — Codex reads from ~/.agents/skills/)
71
+ const HOME = homedir();
72
+ if (cwd === HOME) {
73
+ const agentsSkillsDir = join(cwd, '.agents', 'skills');
74
+ if (existsSync(agentsSkillsDir)) cleanSymlinksRecursive(agentsSkillsDir);
75
+ }
72
76
  }
73
77
 
74
78
  /**
@@ -184,20 +188,22 @@ export function linkWorkspace(cwd) {
184
188
  }
185
189
  }
186
190
 
187
- // Codex per-skill symlinks: .agents/skills/<name>
188
- const agentsSkillsDir = join(cwd, '.agents/skills');
189
- for (const ns of namespaces) {
190
- for (const { typeDirPath: skillsDir, segments } of findNestedTypeDirs(join(awDir, ns), 'skills')) {
191
- mkdirSync(agentsSkillsDir, { recursive: true });
192
- for (const skill of listDirs(skillsDir)) {
193
- const flat = [ns, ...segments, skill].join('-');
194
- const linkPath = join(agentsSkillsDir, flat);
195
- const targetPath = join(skillsDir, skill);
196
- const relTarget = relative(agentsSkillsDir, targetPath);
197
- try {
198
- execSync(`ln -sfn "${relTarget}" "${linkPath}"`, { stdio: 'pipe' });
199
- created++;
200
- } catch { /* best effort */ }
191
+ // Codex per-skill symlinks: ~/.agents/skills/<name> (global only)
192
+ if (cwd === homedir()) {
193
+ const agentsSkillsDir = join(cwd, '.agents/skills');
194
+ for (const ns of namespaces) {
195
+ for (const { typeDirPath: skillsDir, segments } of findNestedTypeDirs(join(awDir, ns), 'skills')) {
196
+ mkdirSync(agentsSkillsDir, { recursive: true });
197
+ for (const skill of listDirs(skillsDir)) {
198
+ const flat = [ns, ...segments, skill].join('-');
199
+ const linkPath = join(agentsSkillsDir, flat);
200
+ const targetPath = join(skillsDir, skill);
201
+ const relTarget = relative(agentsSkillsDir, targetPath);
202
+ try {
203
+ execSync(`ln -sfn "${relTarget}" "${linkPath}"`, { stdio: 'pipe' });
204
+ created++;
205
+ } catch { /* best effort */ }
206
+ }
201
207
  }
202
208
  }
203
209
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghl-ai/aw",
3
- "version": "0.1.25-beta.1",
3
+ "version": "0.1.25-beta.11",
4
4
  "description": "Agentic Workspace CLI — pull, push & manage agents, skills and commands from the registry",
5
5
  "type": "module",
6
6
  "bin": {
package/plan.mjs CHANGED
@@ -24,11 +24,12 @@ export function computePlan(registryDirs, workspaceDir, includePatterns = [], {
24
24
  continue;
25
25
  }
26
26
 
27
- // For skill files: key includes relative path to avoid collisions
28
- // For other files: key is type/slug
27
+ // Key must include namespacePath to avoid collisions when the same
28
+ // slug exists under different domains (e.g. backend/agents/developer
29
+ // vs frontend/agents/developer).
29
30
  const key = entry.skillRelPath
30
- ? `${entry.type}/${entry.slug}/${entry.skillRelPath}`
31
- : `${entry.type}/${entry.slug}`;
31
+ ? `${entry.namespacePath}/${entry.type}/${entry.slug}/${entry.skillRelPath}`
32
+ : `${entry.namespacePath}/${entry.type}/${entry.slug}`;
32
33
  plan.set(key, { ...entry, source: entry.namespacePath || name });
33
34
  }
34
35
  }
@@ -108,9 +109,10 @@ export function computePlan(registryDirs, workspaceDir, includePatterns = [], {
108
109
  }
109
110
  }
110
111
  if (typeIdx === -1) continue;
112
+ const namespace = parts.slice(0, typeIdx).join('/');
111
113
  const type = parts[typeIdx];
112
114
  const slug = parts[typeIdx + 1]?.replace(/\.md$/, '');
113
- const key = `${type}/${slug}`;
115
+ const key = `${namespace}/${type}/${slug}`;
114
116
  if (!plan.has(key)) {
115
117
  actions.push({
116
118
  slug,