@ghl-ai/aw 0.1.61 → 0.1.62-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.
@@ -36,7 +36,7 @@ import {
36
36
  symlinkSync,
37
37
  unlinkSync,
38
38
  } from 'node:fs';
39
- import { join, basename, extname } from 'node:path';
39
+ import { join, basename, extname, relative } from 'node:path';
40
40
 
41
41
  // Canonical AW routing-stage commands. Anything in <eccHome>/commands/*.md
42
42
  // not on this list is treated as "not a slash-command" (e.g. README.md).
@@ -230,6 +230,109 @@ export function ensureCommandSurface(opts) {
230
230
  };
231
231
  }
232
232
 
233
+ /**
234
+ * Link ALL registry commands from ~/.aw/.aw_registry into the harness
235
+ * command directory — not just the 10 stage commands.
236
+ *
237
+ * `ensureCommandSurface` only links ECC stage commands (plan, build, etc.).
238
+ * The full registry (synced by `aw init`) contains 100+ domain-specific
239
+ * commands (e.g. platform-review-security-hardening) that were invisible
240
+ * in cloud harnesses because nothing linked them.
241
+ *
242
+ * This function mirrors the logic in link.mjs::linkWorkspace's command
243
+ * section but is callable from the c4 orchestrator without importing the
244
+ * full link surface.
245
+ *
246
+ * @param {object} opts
247
+ * @param {'claude-web'|'cursor-cloud'|'codex-web'|string} opts.harness
248
+ * @param {string} opts.home
249
+ * @param {string} opts.awRegistryDir e.g. ~/.aw/.aw_registry
250
+ * @returns {{ linked: number, skipped: number, harness: string }}
251
+ */
252
+ export function ensureRegistryCommandSurface(opts) {
253
+ if (!opts || typeof opts !== 'object') {
254
+ throw new Error('ensureRegistryCommandSurface: opts object is required');
255
+ }
256
+ const { harness, home, awRegistryDir } = opts;
257
+
258
+ if (harness === 'claude-web') {
259
+ return { linked: 0, skipped: 0, harness, installedAction: 'noop' };
260
+ }
261
+
262
+ const targetDir = harnessTargetDir(harness, home);
263
+ if (!targetDir || !existsSync(awRegistryDir)) {
264
+ return { linked: 0, skipped: 0, harness, installedAction: 'unsupported' };
265
+ }
266
+
267
+ try {
268
+ mkdirSync(targetDir, { recursive: true });
269
+ } catch {
270
+ return { linked: 0, skipped: 0, harness, installedAction: 'symlink' };
271
+ }
272
+
273
+ let linked = 0;
274
+ let skipped = 0;
275
+
276
+ const namespaces = safeReaddir(awRegistryDir).filter(
277
+ (d) => !d.startsWith('.') && isDir(join(awRegistryDir, d)),
278
+ );
279
+
280
+ for (const ns of namespaces) {
281
+ for (const { dir: commandsDir, segments } of findCommandDirs(join(awRegistryDir, ns))) {
282
+ for (const file of safeReaddir(commandsDir).filter((f) => f.endsWith('.md') && !f.startsWith('.'))) {
283
+ const cmdFileName = [ns, ...segments, file].join('-');
284
+ const sourcePath = join(commandsDir, file);
285
+ const linkPath = join(targetDir, cmdFileName);
286
+
287
+ if (isCorrectSymlink(linkPath, sourcePath)) {
288
+ skipped++;
289
+ continue;
290
+ }
291
+
292
+ try {
293
+ if (existsSync(linkPath) || lstatExists(linkPath)) {
294
+ try { unlinkSync(linkPath); } catch { /* stale entry */ }
295
+ }
296
+ symlinkSync(sourcePath, linkPath);
297
+ linked++;
298
+ } catch {
299
+ skipped++;
300
+ }
301
+ }
302
+ }
303
+ }
304
+
305
+ return { linked, skipped, harness, installedAction: 'symlink' };
306
+ }
307
+
308
+ function safeReaddir(dir) {
309
+ try { return readdirSync(dir); } catch { return []; }
310
+ }
311
+
312
+ function isDir(p) {
313
+ try { return lstatSync(p).isDirectory(); } catch { return false; }
314
+ }
315
+
316
+ /**
317
+ * Recursively find `commands/` directories under a namespace dir.
318
+ * Supports nested domain dirs (e.g. platform/review/commands/).
319
+ */
320
+ function findCommandDirs(nsDir, segments = []) {
321
+ const results = [];
322
+ const commandsDir = join(nsDir, 'commands');
323
+ if (existsSync(commandsDir) && isDir(commandsDir)) {
324
+ results.push({ dir: commandsDir, segments });
325
+ }
326
+ for (const entry of safeReaddir(nsDir)) {
327
+ if (entry === 'commands' || entry.startsWith('.')) continue;
328
+ const sub = join(nsDir, entry);
329
+ if (isDir(sub)) {
330
+ results.push(...findCommandDirs(sub, [...segments, entry]));
331
+ }
332
+ }
333
+ return results;
334
+ }
335
+
233
336
  /**
234
337
  * Read-only diagnostic. Walks the harness command directory and reports
235
338
  * which AW stage commands are resolvable. Does not fix.
package/c4/index.mjs CHANGED
@@ -49,7 +49,7 @@ export {
49
49
  export { MCP_URL_DEFAULT, registerGhlAiMcp } from './mcpServer.mjs';
50
50
  export { probeMcpServer } from './mcpSmokeProbe.mjs';
51
51
  export { ensureClaudeMarketplace } from './claudePluginRegistry.mjs';
52
- export { ensureCommandSurface, diagnoseCommandResolution } from './commandSurface.mjs';
52
+ export { ensureCommandSurface, ensureRegistryCommandSurface, diagnoseCommandResolution } from './commandSurface.mjs';
53
53
  export { installCursorSlashShim, CURSOR_SLASH_SHIM_RULE } from './cursorRulesShim.mjs';
54
54
  export { ensureRepoLocalClaudeSettings } from './repoLocalClaudeSettings.mjs';
55
55
  export { copyRepoRootInstructions } from './repoRootInstructions.mjs';
@@ -111,5 +111,48 @@ AW_PACKAGE="${AW_PACKAGE:-@ghl-ai/aw@latest}"
111
111
  echo "[aw-c4-bootstrap] installing ${AW_PACKAGE}"
112
112
  npm install -g "${AW_PACKAGE}"
113
113
 
114
+ # --dry-run and --diagnose are read-only modes; skip post-c4 verification
115
+ # so we don't mutate $HOME or contact the registry unexpectedly.
116
+ is_readonly_mode=false
117
+ for arg in "$@"; do
118
+ case "$arg" in --dry-run|--diagnose) is_readonly_mode=true ;; esac
119
+ done
120
+
121
+ if "$is_readonly_mode"; then
122
+ echo "[aw-c4-bootstrap] running: aw c4 $*"
123
+ exec aw c4 "$@"
124
+ fi
125
+
114
126
  echo "[aw-c4-bootstrap] running: aw c4 $*"
115
- exec aw c4 "$@"
127
+ aw c4 "$@"
128
+ aw_c4_exit=$?
129
+
130
+ # Defense-in-depth: `aw c4` delegates registry sync to `aw init --silent`,
131
+ # which can fail to fetch in silent mode without surfacing an error. When
132
+ # that happens, the registry (~/.aw/.aw_registry) may be empty or incomplete
133
+ # and only the 10 hardcoded stage commands get linked. Re-run `aw pull`
134
+ # post-c4 if the command count looks low.
135
+ verify_registry_commands() {
136
+ local aw_cmd_dir="$HOME/.aw/.aw_registry"
137
+ if [ ! -d "$aw_cmd_dir" ]; then
138
+ echo "[aw-c4-bootstrap] registry dir missing — running aw init + pull"
139
+ aw init --no-integrations --silent 2>&1 | tail -3 || true
140
+ aw pull 2>&1 | tail -5 || true
141
+ return
142
+ fi
143
+
144
+ local cmd_count
145
+ cmd_count=$(find "$aw_cmd_dir" -name '*.md' -path '*/commands/*' ! -path '*/evals/*' 2>/dev/null | wc -l | tr -d ' ')
146
+ if [ "${cmd_count:-0}" -lt 20 ]; then
147
+ echo "[aw-c4-bootstrap] only ${cmd_count} registry commands found — running aw pull"
148
+ aw pull 2>&1 | tail -5 || true
149
+ local new_count
150
+ new_count=$(find "$aw_cmd_dir" -name '*.md' -path '*/commands/*' ! -path '*/evals/*' 2>/dev/null | wc -l | tr -d ' ')
151
+ echo "[aw-c4-bootstrap] registry commands: ${cmd_count} → ${new_count}"
152
+ else
153
+ echo "[aw-c4-bootstrap] registry: ${cmd_count} commands OK"
154
+ fi
155
+ }
156
+ verify_registry_commands || true
157
+
158
+ exit "$aw_c4_exit"
package/commands/c4.mjs CHANGED
@@ -24,7 +24,7 @@
24
24
  */
25
25
 
26
26
  import { spawnSync as nodeSpawnSync } from 'node:child_process';
27
- import { existsSync as fsExistsSync } from 'node:fs';
27
+ import { existsSync as fsExistsSync, readdirSync, statSync } from 'node:fs';
28
28
  import { join } from 'node:path';
29
29
  import * as c4Default from '../c4/index.mjs';
30
30
  import { isMcpEnabled } from '../mcp.mjs';
@@ -97,6 +97,19 @@ function safe(label, fn, writer) {
97
97
  }
98
98
  }
99
99
 
100
+ function safeReaddirSync(dir) {
101
+ try { return readdirSync(dir); } catch { return []; }
102
+ }
103
+
104
+ function safeListNamespaceDirs(dir) {
105
+ try {
106
+ return readdirSync(dir).filter((entry) => {
107
+ if (entry.startsWith('.')) return false;
108
+ try { return statSync(join(dir, entry)).isDirectory(); } catch { return false; }
109
+ });
110
+ } catch { return []; }
111
+ }
112
+
100
113
  async function safeAsync(label, fn, writer) {
101
114
  try {
102
115
  return { ok: true, value: await fn() };
@@ -392,6 +405,22 @@ export async function c4Command(rawArgs, overrides = {}) {
392
405
  return exit(1);
393
406
  }
394
407
 
408
+ // Step 9a — verify registry has namespace directories. `aw init --silent`
409
+ // can exit 0 but fail to fetch (fetchAndMerge swallows the error in silent
410
+ // mode). Metadata-only artifacts (AW-PROTOCOL.md) pass a simple non-empty
411
+ // check, so we specifically look for directories (namespaces like
412
+ // "platform") to confirm the registry content was actually pulled.
413
+ {
414
+ const registryNamespaces = safeListNamespaceDirs(awRegistry);
415
+ if (registryNamespaces.length === 0) {
416
+ writer.stderr('[aw-c4] registry has no namespace directories after init — retrying with aw pull\n');
417
+ const pullRes = spawnSync('aw', ['pull'], { stdio: 'pipe' });
418
+ if (pullRes && pullRes.status !== 0) {
419
+ writer.stderr('[aw-c4] aw pull also failed; commands may be incomplete\n');
420
+ }
421
+ }
422
+ }
423
+
395
424
  // Step 10 — per-harness branch.
396
425
  let branch;
397
426
  try {
@@ -409,9 +438,27 @@ export async function c4Command(rawArgs, overrides = {}) {
409
438
  writer.stdout('[aw-c4] MCP disabled; skipping registerGhlAiMcp\n');
410
439
  }
411
440
 
412
- // Step 12 — slash command surface.
441
+ // Step 12 — slash command surface (10 stage commands from ECC).
413
442
  safe('ensureCommandSurface', () => c4.ensureCommandSurface({ harness, home, eccHome }), writer);
414
443
 
444
+ // Step 12a — full registry command surface.
445
+ // `ensureCommandSurface` only links the 10 AW routing-stage commands from
446
+ // ~/.aw-ecc/commands/. The full registry (~100+ domain commands like
447
+ // platform-review-security-hardening) lives in ~/.aw/.aw_registry/ and is
448
+ // populated by `aw init` (step 7). Link them all into the harness command
449
+ // dir so `/aw:*` resolution works for every registered command.
450
+ const registryCmdResult = safe(
451
+ 'ensureRegistryCommandSurface',
452
+ () => c4.ensureRegistryCommandSurface({ harness, home, awRegistryDir: awRegistry }),
453
+ writer,
454
+ );
455
+ if (registryCmdResult.ok) {
456
+ const { linked, skipped } = registryCmdResult.value;
457
+ if (linked > 0) {
458
+ writer.stdout(`[aw-c4] registry commands: ${linked} linked, ${skipped} skipped\n`);
459
+ }
460
+ }
461
+
415
462
  // Step 12b — Cursor Cloud slash-expand rule (no-op on other harnesses).
416
463
  // The model-side workaround for Cursor Cloud's chat UI not pre-expanding
417
464
  // `/aw:<NAME>` slash commands. The function self-skips on other
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghl-ai/aw",
3
- "version": "0.1.61",
3
+ "version": "0.1.62-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": {