@ghl-ai/aw 0.1.43-beta.0 → 0.1.43-beta.1

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/commands/init.mjs CHANGED
@@ -6,6 +6,7 @@
6
6
 
7
7
  import {
8
8
  existsSync,
9
+ mkdirSync,
9
10
  writeFileSync,
10
11
  symlinkSync,
11
12
  lstatSync,
@@ -90,6 +91,22 @@ function syncHomeAndProjectInstructions(cwd, namespace) {
90
91
  }
91
92
  }
92
93
 
94
+ /**
95
+ * Create scaffold directories for a new team namespace so the developer
96
+ * has the standard folder structure ready for agents, skills, commands, and evals.
97
+ * No-op if the directories already exist.
98
+ */
99
+ function scaffoldNamespace(awHome, folderName) {
100
+ const nsDir = join(awHome, REGISTRY_DIR, ...folderName.split('/'));
101
+ for (const type of ['agents', 'skills', 'commands', 'evals']) {
102
+ const dir = join(nsDir, type);
103
+ if (!existsSync(dir)) {
104
+ mkdirSync(dir, { recursive: true });
105
+ writeFileSync(join(dir, '.gitkeep'), '');
106
+ }
107
+ }
108
+ }
109
+
93
110
  // ── Ensure ~/.aw/.git/info/exclude has the whitelist block ─────────────
94
111
  //
95
112
  // Strategy: only .aw_registry/, .aw_rules/, content/ are tracked — everything
@@ -309,6 +326,7 @@ export async function initCommand(args) {
309
326
  const newSparsePaths = [`.aw_registry/${folderName}`, 'content', RULES_SOURCE_DIR];
310
327
  addToSparseCheckout(AW_HOME, newSparsePaths);
311
328
  config.addPattern(GLOBAL_AW_DIR, folderName);
329
+ scaffoldNamespace(AW_HOME, folderName);
312
330
  } else {
313
331
  if (!silent) fmt.logStep('Already initialized — syncing...');
314
332
  }
@@ -486,6 +504,7 @@ export async function initCommand(args) {
486
504
  const cfg = config.create(GLOBAL_AW_DIR, { namespace: team || 'platform', user });
487
505
  if (folderName) {
488
506
  config.addPattern(GLOBAL_AW_DIR, folderName);
507
+ scaffoldNamespace(AW_HOME, folderName);
489
508
  }
490
509
  // Parallel batch A: rules sync (HOME + cwd are independent targets)
491
510
  syncRulesTargets(HOME);
package/commands/push.mjs CHANGED
@@ -13,7 +13,7 @@ import * as fmt from '../fmt.mjs';
13
13
  import { chalk } from '../fmt.mjs';
14
14
  import { REGISTRY_REPO, REGISTRY_URL, REGISTRY_BASE_BRANCH, REGISTRY_DIR, AW_CO_AUTHOR } from '../constants.mjs';
15
15
  import { resolveInput } from '../paths.mjs';
16
- import { walkRegistryTree } from '../registry.mjs';
16
+ import { walkRegistryTree, getAllFiles } from '../registry.mjs';
17
17
  import {
18
18
  detectChanges,
19
19
  getStagedFiles,
@@ -236,9 +236,11 @@ function collectBatchFiles(folderAbsPath, registrySubDir) {
236
236
  return true;
237
237
  })
238
238
  .map(entry => {
239
- const registryTarget = (entry.type === 'skills' || entry.type === 'evals')
240
- ? `${REGISTRY_DIR}/${entry.namespacePath}/${entry.type}/${entry.slug}/${entry.skillRelPath || entry.filename}`
241
- : `${REGISTRY_DIR}/${entry.namespacePath}/${entry.type}/${entry.filename}`;
239
+ const registryTarget = entry.evalRelPath
240
+ ? `${REGISTRY_DIR}/${entry.namespacePath}/${entry.type}/${entry.evalRelPath}`
241
+ : (entry.type === 'skills' || entry.type === 'evals')
242
+ ? `${REGISTRY_DIR}/${entry.namespacePath}/${entry.type}/${entry.slug}/${entry.skillRelPath || entry.filename}`
243
+ : `${REGISTRY_DIR}/${entry.namespacePath}/${entry.type}/${entry.filename}`;
242
244
  return {
243
245
  absPath: entry.sourcePath,
244
246
  registryTarget,
@@ -267,6 +269,36 @@ function parseRegistryPath(relPath) {
267
269
  return null;
268
270
  }
269
271
 
272
+ // ── Colocated eval resolution ─────────────────────────────────────────
273
+
274
+ /**
275
+ * Find colocated eval files for a given agent or command.
276
+ * Evals live at <type>/evals/<slug>/eval-*.md alongside their parent artifact.
277
+ * Only returns files that have pending changes (present in changedPaths).
278
+ */
279
+ function resolveColocatedEvals(registrySubDir, namespace, parentType, slug, changedPaths) {
280
+ if (parentType !== 'agents' && parentType !== 'commands') return [];
281
+
282
+ const evalsDir = join(registrySubDir, namespace, parentType, 'evals', slug);
283
+ if (!existsSync(evalsDir) || !statSync(evalsDir).isDirectory()) return [];
284
+
285
+ return getAllFiles(evalsDir)
286
+ .map(file => {
287
+ const relFromRegistryBase = file.slice(registrySubDir.length + 1);
288
+ const registryTarget = `${REGISTRY_DIR}/${relFromRegistryBase}`;
289
+ return { absPath: file, registryTarget };
290
+ })
291
+ .filter(f => changedPaths.has(f.registryTarget))
292
+ .map(f => ({
293
+ ...f,
294
+ type: parentType,
295
+ namespace,
296
+ slug,
297
+ isDir: false,
298
+ deleted: false,
299
+ }));
300
+ }
301
+
270
302
  // ── CODEOWNERS helpers ────────────────────────────────────────────────
271
303
 
272
304
  async function getGitHubUser() {
@@ -727,7 +759,7 @@ export async function pushCommand(args) {
727
759
  return;
728
760
  }
729
761
 
730
- await doPush([{
762
+ const mainFile = {
731
763
  absPath,
732
764
  registryTarget,
733
765
  type: parentDir,
@@ -735,7 +767,11 @@ export async function pushCommand(args) {
735
767
  slug,
736
768
  isDir,
737
769
  deleted: isDeletedFile,
738
- }], awHome, dryRun, worktreeFlow);
770
+ };
771
+
772
+ // Include colocated evals for agents/commands (agents/evals/<slug>/*, commands/evals/<slug>/*)
773
+ const colocatedEvals = resolveColocatedEvals(registrySubDir, namespacePath, parentDir, slug, singleChangedPaths);
774
+ await doPush([mainFile, ...colocatedEvals], awHome, dryRun, worktreeFlow);
739
775
  }
740
776
 
741
777
  // ── Utilities ─────────────────────────────────────────────────────────
package/link.mjs CHANGED
@@ -47,6 +47,8 @@ function listDirs(dir) {
47
47
  * platform/frontend/agents/ → { typeDirPath: '.../frontend/agents', segments: ['frontend'] }
48
48
  */
49
49
  function findNestedTypeDirs(nsDir, typeName) {
50
+ // Types whose directories may contain colocated evals
51
+ const COLOCATED_EVAL_PARENTS = new Set(['agents', 'commands']);
50
52
  const results = [];
51
53
  function walk(dir, segments) {
52
54
  if (!existsSync(dir)) return;
@@ -56,6 +58,8 @@ function findNestedTypeDirs(nsDir, typeName) {
56
58
  results.push({ typeDirPath: join(dir, entry.name), segments });
57
59
  } else if (!ALL_KNOWN_TYPES.has(entry.name)) {
58
60
  walk(join(dir, entry.name), [...segments, entry.name]);
61
+ } else if (typeName === 'evals' && COLOCATED_EVAL_PARENTS.has(entry.name)) {
62
+ walk(join(dir, entry.name), [...segments, entry.name]);
59
63
  }
60
64
  }
61
65
  }
@@ -187,20 +191,37 @@ export function linkWorkspace(cwd, awDirOverride = null, { silent = false } = {}
187
191
  // Evals: per-eval-dir symlinks (recursive for nested domain dirs)
188
192
  for (const ns of namespaces) {
189
193
  for (const { typeDirPath: evalsDir, segments } of findNestedTypeDirs(join(awDir, ns), 'evals')) {
190
- for (const subType of listDirs(evalsDir)) {
191
- const subDir = join(evalsDir, subType);
192
- for (const evalName of listDirs(subDir)) {
193
- const flat = [ns, ...segments, subType, evalName].join('-');
194
+ const isColocated = segments.some(s => s === 'agents' || s === 'commands');
194
195
 
196
+ if (isColocated) {
197
+ // Colocated evals: agents/evals/<slug>/ — one level of slug dirs
198
+ for (const evalSlug of listDirs(evalsDir)) {
199
+ const flat = [ns, ...segments, evalSlug].join('-');
195
200
  for (const ide of IDE_DIRS) {
196
201
  const linkDir = join(cwd, ide, 'evals');
197
202
  mkdirSync(linkDir, { recursive: true });
198
203
  const linkPath = join(linkDir, flat);
199
- const targetPath = join(subDir, evalName);
204
+ const targetPath = join(evalsDir, evalSlug);
200
205
  const relTarget = relative(linkDir, targetPath);
201
206
  try { forceSymlink(relTarget, linkPath); created++; } catch { /* best effort */ }
202
207
  }
203
208
  }
209
+ } else {
210
+ // Standard evals: evals/<subType>/<slug>/ — two levels of dirs
211
+ for (const subType of listDirs(evalsDir)) {
212
+ const subDir = join(evalsDir, subType);
213
+ for (const evalName of listDirs(subDir)) {
214
+ const flat = [ns, ...segments, subType, evalName].join('-');
215
+ for (const ide of IDE_DIRS) {
216
+ const linkDir = join(cwd, ide, 'evals');
217
+ mkdirSync(linkDir, { recursive: true });
218
+ const linkPath = join(linkDir, flat);
219
+ const targetPath = join(subDir, evalName);
220
+ const relTarget = relative(linkDir, targetPath);
221
+ try { forceSymlink(relTarget, linkPath); created++; } catch { /* best effort */ }
222
+ }
223
+ }
224
+ }
204
225
  }
205
226
  }
206
227
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghl-ai/aw",
3
- "version": "0.1.43-beta.0",
3
+ "version": "0.1.43-beta.1",
4
4
  "description": "Agentic Workspace CLI — pull, push & manage agents, skills and commands from the registry",
5
5
  "type": "module",
6
6
  "bin": {
package/registry.mjs CHANGED
@@ -84,18 +84,37 @@ export function walkRegistryTree(baseDir, baseName) {
84
84
  });
85
85
  }
86
86
  } else {
87
- // Agents, commands — flat files
87
+ // Agents, commands — flat files + colocated evals (same pattern as skills)
88
88
  for (const fileEntry of readdirSync(fullPath)) {
89
89
  if (fileEntry === '.gitkeep' || fileEntry.startsWith('.')) continue;
90
90
  const filePath = join(fullPath, fileEntry);
91
- if (!statSync(filePath).isFile()) continue;
92
- const slug = fileEntry.replace(/\.md$/, '');
93
- const registryPath = `${namespace}/${typeDir}/${slug}`;
94
- entries.push({
95
- slug, type: typeDir, sourcePath: filePath,
96
- isDirectory: false, namespacePath: namespace, registryPath,
97
- filename: fileEntry,
98
- });
91
+ if (statSync(filePath).isFile()) {
92
+ const slug = fileEntry.replace(/\.md$/, '');
93
+ const registryPath = `${namespace}/${typeDir}/${slug}`;
94
+ entries.push({
95
+ slug, type: typeDir, sourcePath: filePath,
96
+ isDirectory: false, namespacePath: namespace, registryPath,
97
+ filename: fileEntry,
98
+ });
99
+ } else if (fileEntry === 'evals' && statSync(filePath).isDirectory()) {
100
+ for (const file of getAllFiles(filePath)) {
101
+ const relFromType = relative(fullPath, file);
102
+ const relFromEvals = relative(filePath, file);
103
+ const parts = relFromEvals.split('/');
104
+ const parentSlug = parts.length > 1 ? parts[0] : '';
105
+ const filename = parts[parts.length - 1];
106
+ entries.push({
107
+ slug: parentSlug || filename.replace(/\.md$/, ''),
108
+ type: typeDir,
109
+ sourcePath: file,
110
+ isDirectory: false,
111
+ namespacePath: namespace,
112
+ registryPath: `${namespace}/${typeDir}/${relFromType.replace(/\.md$/, '')}`,
113
+ filename,
114
+ evalRelPath: relFromType,
115
+ });
116
+ }
117
+ }
99
118
  }
100
119
  }
101
120
  } else if (entry.isDirectory() && !SKIP_DIRS.has(entry.name)) {