@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.
- package/c4/commandSurface.mjs +104 -1
- package/c4/index.mjs +1 -1
- package/c4/templates/scripts/aw-c4-bootstrap.sh +44 -1
- package/commands/c4.mjs +49 -2
- package/package.json +1 -1
package/c4/commandSurface.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|