@ghl-ai/aw 0.1.61 → 0.1.62-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.
@@ -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';
@@ -112,4 +112,35 @@ echo "[aw-c4-bootstrap] installing ${AW_PACKAGE}"
112
112
  npm install -g "${AW_PACKAGE}"
113
113
 
114
114
  echo "[aw-c4-bootstrap] running: aw c4 $*"
115
- exec aw c4 "$@"
115
+ aw c4 "$@"
116
+ aw_c4_exit=$?
117
+
118
+ # Defense-in-depth: `aw c4` delegates registry sync to `aw init --silent`,
119
+ # which can fail to fetch in silent mode without surfacing an error. When
120
+ # that happens, the registry (~/.aw/.aw_registry) may be empty or incomplete
121
+ # and only the 10 hardcoded stage commands get linked. Re-run `aw pull`
122
+ # post-c4 if the command count looks low.
123
+ verify_registry_commands() {
124
+ local aw_cmd_dir="$HOME/.aw/.aw_registry"
125
+ if [ ! -d "$aw_cmd_dir" ]; then
126
+ echo "[aw-c4-bootstrap] registry dir missing — running aw init + pull"
127
+ aw init --no-integrations --silent 2>&1 | tail -3 || true
128
+ aw pull 2>&1 | tail -5 || true
129
+ return
130
+ fi
131
+
132
+ local cmd_count
133
+ cmd_count=$(find "$aw_cmd_dir" -name '*.md' -path '*/commands/*' 2>/dev/null | wc -l | tr -d ' ')
134
+ if [ "${cmd_count:-0}" -lt 20 ]; then
135
+ echo "[aw-c4-bootstrap] only ${cmd_count} registry commands found — running aw pull"
136
+ aw pull 2>&1 | tail -5 || true
137
+ local new_count
138
+ new_count=$(find "$aw_cmd_dir" -name '*.md' -path '*/commands/*' 2>/dev/null | wc -l | tr -d ' ')
139
+ echo "[aw-c4-bootstrap] registry commands: ${cmd_count} → ${new_count}"
140
+ else
141
+ echo "[aw-c4-bootstrap] registry: ${cmd_count} commands OK"
142
+ fi
143
+ }
144
+ verify_registry_commands || true
145
+
146
+ 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 } 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,10 @@ function safe(label, fn, writer) {
97
97
  }
98
98
  }
99
99
 
100
+ function safeReaddirSync(dir) {
101
+ try { return readdirSync(dir); } catch { return []; }
102
+ }
103
+
100
104
  async function safeAsync(label, fn, writer) {
101
105
  try {
102
106
  return { ok: true, value: await fn() };
@@ -392,6 +396,24 @@ export async function c4Command(rawArgs, overrides = {}) {
392
396
  return exit(1);
393
397
  }
394
398
 
399
+ // Step 9a — verify registry has content. `aw init --silent` can succeed
400
+ // (exit 0) but fail to fetch from remote (fetchAndMerge swallows the error
401
+ // in silent mode). When the registry dir exists but contains no namespace
402
+ // directories, the fetch likely failed. Retry with `aw pull` which runs
403
+ // a targeted git pull on the existing clone.
404
+ {
405
+ const registryEntries = safeReaddirSync(awRegistry).filter(
406
+ (e) => !e.startsWith('.'),
407
+ );
408
+ if (registryEntries.length === 0) {
409
+ writer.stderr('[aw-c4] registry empty after init — retrying with aw pull\n');
410
+ const pullRes = spawnSync('aw', ['pull'], { stdio: 'pipe' });
411
+ if (pullRes && pullRes.status !== 0) {
412
+ writer.stderr('[aw-c4] aw pull also failed; commands may be incomplete\n');
413
+ }
414
+ }
415
+ }
416
+
395
417
  // Step 10 — per-harness branch.
396
418
  let branch;
397
419
  try {
@@ -409,9 +431,27 @@ export async function c4Command(rawArgs, overrides = {}) {
409
431
  writer.stdout('[aw-c4] MCP disabled; skipping registerGhlAiMcp\n');
410
432
  }
411
433
 
412
- // Step 12 — slash command surface.
434
+ // Step 12 — slash command surface (10 stage commands from ECC).
413
435
  safe('ensureCommandSurface', () => c4.ensureCommandSurface({ harness, home, eccHome }), writer);
414
436
 
437
+ // Step 12a — full registry command surface.
438
+ // `ensureCommandSurface` only links the 10 AW routing-stage commands from
439
+ // ~/.aw-ecc/commands/. The full registry (~100+ domain commands like
440
+ // platform-review-security-hardening) lives in ~/.aw/.aw_registry/ and is
441
+ // populated by `aw init` (step 7). Link them all into the harness command
442
+ // dir so `/aw:*` resolution works for every registered command.
443
+ const registryCmdResult = safe(
444
+ 'ensureRegistryCommandSurface',
445
+ () => c4.ensureRegistryCommandSurface({ harness, home, awRegistryDir: awRegistry }),
446
+ writer,
447
+ );
448
+ if (registryCmdResult.ok) {
449
+ const { linked, skipped } = registryCmdResult.value;
450
+ if (linked > 0) {
451
+ writer.stdout(`[aw-c4] registry commands: ${linked} linked, ${skipped} skipped\n`);
452
+ }
453
+ }
454
+
415
455
  // Step 12b — Cursor Cloud slash-expand rule (no-op on other harnesses).
416
456
  // The model-side workaround for Cursor Cloud's chat UI not pre-expanding
417
457
  // `/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.0",
4
4
  "description": "Agentic Workspace CLI — pull, push & manage agents, skills and commands from the registry",
5
5
  "type": "module",
6
6
  "bin": {