@ghl-ai/aw 0.1.70-beta.0 → 0.1.70-beta.2

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.
@@ -12,7 +12,7 @@
12
12
  *
13
13
  * 2. enabledPlugins["aw@aw-marketplace"] = true
14
14
  * Activates the plugin (without this Claude knows about the marketplace
15
- * but does not surface its commands/skills).
15
+ * but does not surface its routing hooks).
16
16
  *
17
17
  * 3. permissions.allow includes "Skill"
18
18
  * Without this Claude refuses to dispatch the Skill tool. Append-only
@@ -114,8 +114,8 @@ function ensureSkillPermission(root) {
114
114
  }
115
115
 
116
116
  /**
117
- * Ensure Claude is wired up to surface ECC's `aw:<stage>` skills via plugin
118
- * marketplace + Skill permission.
117
+ * Ensure Claude is wired up to the AW plugin marketplace + Skill permission.
118
+ * Active AW skill bodies come from ~/.aw/.aw_registry/aw/skills.
119
119
  *
120
120
  * @param {string} home User home (e.g. tmp dir in tests).
121
121
  * @param {string} eccDir Path to the ECC plugin directory (~/.aw-ecc).
@@ -1,30 +1,21 @@
1
1
  /**
2
- * c4/commandSurface.mjs — slash-command resolution per harness (G3).
2
+ * c4/commandSurface.mjs — slash-command resolution per harness.
3
3
  *
4
- * Why: intent-based routing fires via the slim card / SessionStart hook,
5
- * but a user typing `/aw:plan` directly relies on a per-harness slash
6
- * command surface. Without these symlinks (or, on Claude, the plugin
7
- * marketplace registration), `/aw:plan` errors out and the AC for
8
- * "manual command works" fails.
4
+ * `/aw:*` now resolves to AW skills directly. The active source of truth is
5
+ * `~/.aw/.aw_registry/aw/skills/<skill>/SKILL.md`; command markdown from
6
+ * aw-ecc is intentionally not installed into the cloud slash surface.
9
7
  *
10
- * Naming convention (verified, L4): ECC ships UN-PREFIXED command files
11
- * (`plan.md`, `build.md`, …). Slash namespacing is by SUBDIRECTORY:
12
- * ~/.cursor/commands/aw/<name>.md /aw:<name>
13
- * ~/.codex/commands/aw/<name>.md → /aw:<name>
14
- * The legacy `~/.cursor/commands/aw-<name>.md` shape is NOT what the
15
- * pilot transcripts produced and is not what we install.
8
+ * Naming convention:
9
+ * ~/.cursor/commands/aw/<name>.md -> /aw:<name>
10
+ * ~/.codex/commands/aw/<name>.md -> /aw:<name>
16
11
  *
17
- * Per-harness behavior:
18
- * - claude-web: 'noop' — plugin marketplace (registered separately by
19
- * claudePluginRegistry.mjs) exposes the ECC commands directly.
20
- * - cursor-cloud / codex-web: 'symlink' — link each ECC command into
21
- * the harness command dir, replacing stale symlinks idempotently.
12
+ * The adapter file is a symlink to the skill's `SKILL.md`:
13
+ * aw-plan/SKILL.md -> /aw:plan
14
+ * grill-with-docs/SKILL.md -> /aw:grill-with-docs
22
15
  *
23
- * `expectedCommands` is derived at runtime by globbing
24
- * `<eccHome>/commands/*.md` and filtering to the AW routing-stage list.
25
- * No hardcoded array drives the install; ECC is the source of truth.
26
- *
27
- * Contract: spec.md::§"c4/commandSurface.mjs", tasks.md::3.7.
16
+ * The `aw-` prefix is stripped only for slash aliases so skill directory
17
+ * names remain stable while user-facing routes stay `/aw:plan`, `/aw:build`,
18
+ * etc. Non-prefixed skill names are exposed unchanged.
28
19
  */
29
20
 
30
21
  import {
@@ -33,48 +24,52 @@ import {
33
24
  mkdirSync,
34
25
  readdirSync,
35
26
  readlinkSync,
27
+ rmSync,
36
28
  symlinkSync,
37
29
  unlinkSync,
38
30
  } from 'node:fs';
39
- import { join, basename, extname, relative } from 'node:path';
40
-
41
- // Canonical AW routing-stage commands. Anything in <eccHome>/commands/*.md
42
- // not on this list is treated as "not a slash-command" (e.g. README.md).
43
- // This list is a filter applied AFTER globbing; it is not a source of truth
44
- // for what gets installed — only what we recognize as a stage.
45
- const AW_STAGE_COMMANDS = new Set([
46
- 'plan',
47
- 'build',
48
- 'investigate',
49
- 'review',
50
- 'test',
51
- 'deploy',
52
- 'ship',
53
- 'feature',
54
- 'adk',
55
- 'publish',
56
- ]);
31
+ import { join } from 'node:path';
32
+
33
+ function awSkillsDir(awHome) {
34
+ return join(awHome, '.aw_registry', 'aw', 'skills');
35
+ }
36
+
37
+ function slashNameForSkill(skillName) {
38
+ return skillName.startsWith('aw-') && skillName.length > 3
39
+ ? skillName.slice(3)
40
+ : skillName;
41
+ }
57
42
 
58
43
  /**
59
- * Walk <eccHome>/commands/*.md and return the AW-stage basenames in
60
- * alphabetical order. Stage filename matching is case-sensitive (ECC ships
61
- * lowercase).
44
+ * Discover active AW skills and their slash aliases.
62
45
  *
63
- * @param {string} eccHome
64
- * @returns {string[]}
46
+ * @param {string} awHome
47
+ * @returns {Array<{ skillName: string, slashName: string, sourcePath: string }>}
65
48
  */
66
- function discoverEccStageCommands(eccHome) {
67
- const commandsDir = join(eccHome, 'commands');
68
- if (!existsSync(commandsDir)) return [];
69
- const entries = readdirSync(commandsDir);
70
- const stageNames = [];
71
- for (const entry of entries) {
72
- if (extname(entry).toLowerCase() !== '.md') continue;
73
- const name = basename(entry, '.md');
74
- if (AW_STAGE_COMMANDS.has(name)) stageNames.push(name);
49
+ function discoverAwSkillRoutes(awHome) {
50
+ const skillsDir = awSkillsDir(awHome);
51
+ if (!existsSync(skillsDir)) return [];
52
+
53
+ const routes = new Map();
54
+ for (const entry of safeReaddir(skillsDir)) {
55
+ if (entry.startsWith('.')) continue;
56
+ const skillDir = join(skillsDir, entry);
57
+ if (!isDir(skillDir)) continue;
58
+ const sourcePath = join(skillDir, 'SKILL.md');
59
+ if (!existsSync(sourcePath)) continue;
60
+
61
+ const slashName = slashNameForSkill(entry);
62
+ const existing = routes.get(slashName);
63
+ const candidate = { skillName: entry, slashName, sourcePath };
64
+ if (!existing || (entry.startsWith('aw-') && !existing.skillName.startsWith('aw-'))) {
65
+ routes.set(slashName, candidate);
66
+ }
75
67
  }
76
- stageNames.sort();
77
- return stageNames;
68
+
69
+ return [...routes.values()].sort((a, b) => {
70
+ const bySlash = a.slashName.localeCompare(b.slashName);
71
+ return bySlash === 0 ? a.skillName.localeCompare(b.skillName) : bySlash;
72
+ });
78
73
  }
79
74
 
80
75
  function harnessTargetDir(harness, home) {
@@ -99,47 +94,79 @@ function isCorrectSymlink(linkPath, expectedTarget) {
99
94
  }
100
95
  }
101
96
 
102
- /**
103
- * Symlink one stage command from ECC into the harness command directory,
104
- * replacing a stale symlink if needed. Returns true on success.
105
- */
106
- function linkStageCommand(stageName, eccHome, targetDir) {
107
- const sourcePath = join(eccHome, 'commands', `${stageName}.md`);
108
- const linkPath = join(targetDir, `${stageName}.md`);
109
- if (isCorrectSymlink(linkPath, sourcePath)) return true;
110
- // Replace any stale symlink or wrong-type entry.
97
+ function lstatExists(p) {
111
98
  try {
112
- if (existsSync(linkPath) || lstatExists(linkPath)) {
113
- try { unlinkSync(linkPath); } catch { /* may be a directory */ }
114
- }
115
- symlinkSync(sourcePath, linkPath);
99
+ lstatSync(p);
116
100
  return true;
117
101
  } catch {
118
102
  return false;
119
103
  }
120
104
  }
121
105
 
122
- function lstatExists(p) {
106
+ function isDir(p) {
123
107
  try {
124
- lstatSync(p);
108
+ return lstatSync(p).isDirectory();
109
+ } catch {
110
+ return false;
111
+ }
112
+ }
113
+
114
+ function safeReaddir(dir) {
115
+ try {
116
+ return readdirSync(dir);
117
+ } catch {
118
+ return [];
119
+ }
120
+ }
121
+
122
+ function linkSkillRoute(route, targetDir) {
123
+ const linkPath = join(targetDir, `${route.slashName}.md`);
124
+ if (isCorrectSymlink(linkPath, route.sourcePath)) return true;
125
+
126
+ try {
127
+ if (existsSync(linkPath) || lstatExists(linkPath)) {
128
+ try { rmSync(linkPath, { recursive: true, force: true }); } catch { /* best effort */ }
129
+ }
130
+ symlinkSync(route.sourcePath, linkPath);
125
131
  return true;
126
132
  } catch {
127
133
  return false;
128
134
  }
129
135
  }
130
136
 
137
+ function pruneStaleSlashAdapters(targetDir, routes) {
138
+ const expectedFiles = new Set(routes.map((route) => `${route.slashName}.md`));
139
+ let pruned = 0;
140
+
141
+ for (const entry of safeReaddir(targetDir)) {
142
+ if (entry.startsWith('.')) continue;
143
+ if (expectedFiles.has(entry)) continue;
144
+
145
+ try {
146
+ rmSync(join(targetDir, entry), { recursive: true, force: true });
147
+ pruned++;
148
+ } catch {
149
+ // Best effort: stale command cleanup must not block skill adapters.
150
+ }
151
+ }
152
+
153
+ return pruned;
154
+ }
155
+
131
156
  /**
132
- * Ensure the harness slash-command surface for AW stages.
157
+ * Ensure the harness slash surface for active AW skills.
133
158
  *
134
159
  * @param {object} opts
135
160
  * @param {'claude-web'|'cursor-cloud'|'codex-web'|string} opts.harness
136
161
  * @param {string} opts.home
137
- * @param {string} opts.eccHome
162
+ * @param {string} opts.awHome
138
163
  * @returns {{
139
164
  * harness: string,
140
165
  * expectedCommands: string[],
166
+ * expectedSkills: string[],
141
167
  * found: string[],
142
168
  * missing: string[],
169
+ * pruned: number,
143
170
  * installedAction: 'symlink' | 'noop' | 'unsupported',
144
171
  * }}
145
172
  */
@@ -147,25 +174,26 @@ export function ensureCommandSurface(opts) {
147
174
  if (!opts || typeof opts !== 'object') {
148
175
  throw new Error('ensureCommandSurface: opts object is required');
149
176
  }
150
- const { harness, home, eccHome } = opts;
177
+ const { harness, home, awHome } = opts;
151
178
  if (!home || typeof home !== 'string') {
152
179
  throw new Error('ensureCommandSurface: opts.home is required');
153
180
  }
154
- if (!eccHome || typeof eccHome !== 'string') {
155
- throw new Error('ensureCommandSurface: opts.eccHome is required');
181
+ if (!awHome || typeof awHome !== 'string') {
182
+ throw new Error('ensureCommandSurface: opts.awHome is required');
156
183
  }
157
184
 
158
- const expectedCommands = discoverEccStageCommands(eccHome);
185
+ const routes = discoverAwSkillRoutes(awHome);
186
+ const expectedCommands = routes.map((route) => route.slashName);
187
+ const expectedSkills = routes.map((route) => route.skillName);
159
188
 
160
189
  if (harness === 'claude-web') {
161
- // Plugin marketplace (handled by claudePluginRegistry) exposes the same
162
- // commands. We only verify resolution; a separate per-repo resolver will
163
- // catch missing-marketplace cases via dumpPostInitState.
164
190
  return {
165
191
  harness,
166
192
  expectedCommands,
193
+ expectedSkills,
167
194
  found: [...expectedCommands],
168
195
  missing: [],
196
+ pruned: 0,
169
197
  installedAction: 'noop',
170
198
  };
171
199
  }
@@ -175,217 +203,114 @@ export function ensureCommandSurface(opts) {
175
203
  return {
176
204
  harness,
177
205
  expectedCommands,
206
+ expectedSkills,
178
207
  found: [],
179
208
  missing: [],
209
+ pruned: 0,
180
210
  installedAction: 'unsupported',
181
211
  };
182
212
  }
183
213
 
184
- // Best-effort directory creation. If the parent path is blocked (e.g. a
185
- // file already occupies the directory location), we cannot symlink and
186
- // every command will surface as missing.
187
214
  try {
188
215
  mkdirSync(targetDir, { recursive: true });
189
216
  } catch {
190
217
  return {
191
218
  harness,
192
219
  expectedCommands,
220
+ expectedSkills,
193
221
  found: [],
194
222
  missing: [...expectedCommands],
223
+ pruned: 0,
195
224
  installedAction: 'symlink',
196
225
  };
197
226
  }
198
- // mkdirSync(recursive:true) is a no-op if a non-directory file exists at
199
- // the target; we must verify the target is actually a directory now.
200
- let isDir = false;
201
- try {
202
- isDir = lstatSync(targetDir).isDirectory();
203
- } catch {
204
- isDir = false;
205
- }
206
- if (!isDir) {
227
+
228
+ if (!isDir(targetDir)) {
207
229
  return {
208
230
  harness,
209
231
  expectedCommands,
232
+ expectedSkills,
210
233
  found: [],
211
234
  missing: [...expectedCommands],
235
+ pruned: 0,
212
236
  installedAction: 'symlink',
213
237
  };
214
238
  }
215
239
 
240
+ const pruned = pruneStaleSlashAdapters(targetDir, routes);
216
241
  const found = [];
217
242
  const missing = [];
218
- for (const stageName of expectedCommands) {
219
- const ok = linkStageCommand(stageName, eccHome, targetDir);
220
- if (ok) found.push(stageName);
221
- else missing.push(stageName);
243
+ for (const route of routes) {
244
+ const ok = linkSkillRoute(route, targetDir);
245
+ if (ok) found.push(route.slashName);
246
+ else missing.push(route.slashName);
222
247
  }
223
248
 
224
249
  return {
225
250
  harness,
226
251
  expectedCommands,
252
+ expectedSkills,
227
253
  found,
228
254
  missing,
255
+ pruned,
229
256
  installedAction: 'symlink',
230
257
  };
231
258
  }
232
259
 
233
260
  /**
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
- if (segments.includes('evals')) return [];
322
-
323
- const results = [];
324
- const commandsDir = join(nsDir, 'commands');
325
- if (existsSync(commandsDir) && isDir(commandsDir)) {
326
- results.push({ dir: commandsDir, segments });
327
- }
328
- for (const entry of safeReaddir(nsDir)) {
329
- if (entry === 'commands' || entry === 'evals' || entry.startsWith('.')) continue;
330
- const sub = join(nsDir, entry);
331
- if (isDir(sub)) {
332
- results.push(...findCommandDirs(sub, [...segments, entry]));
333
- }
334
- }
335
- return results;
336
- }
337
-
338
- /**
339
- * Read-only diagnostic. Walks the harness command directory and reports
340
- * which AW stage commands are resolvable. Does not fix.
261
+ * Read-only diagnostic. For Cursor/Codex, verifies that slash adapter files
262
+ * point to the active AW skill `SKILL.md` files. Claude is skill/plugin
263
+ * routed and does not require adapter files.
341
264
  *
342
265
  * @param {object} opts
343
266
  * @param {'claude-web'|'cursor-cloud'|'codex-web'|string} opts.harness
344
267
  * @param {string} opts.home
268
+ * @param {string} [opts.awHome]
345
269
  * @returns {{ expected: string[], found: string[], missing: string[], ok: boolean }}
346
270
  */
347
271
  export function diagnoseCommandResolution(opts) {
348
272
  if (!opts || typeof opts !== 'object') {
349
273
  throw new Error('diagnoseCommandResolution: opts object is required');
350
274
  }
351
- const { harness, home } = opts;
275
+ const { harness, home, awHome } = opts;
352
276
  if (!home || typeof home !== 'string') {
353
277
  throw new Error('diagnoseCommandResolution: opts.home is required');
354
278
  }
355
- const expected = [...AW_STAGE_COMMANDS].sort();
279
+
280
+ const routes = awHome && typeof awHome === 'string' ? discoverAwSkillRoutes(awHome) : [];
281
+ const expected = routes.map((route) => route.slashName);
356
282
 
357
283
  if (harness === 'claude-web') {
358
- // Plugin marketplace path. If a `~/.claude/commands` dir exists we
359
- // honor it; otherwise we trust marketplace dispatch and report ok.
360
- const claudeDir = join(home, '.claude/commands');
361
- if (!existsSync(claudeDir)) {
362
- return { expected, found: [], missing: [], ok: true };
363
- }
364
- return walkCommandsDir(claudeDir, expected);
284
+ return { expected, found: [...expected], missing: [], ok: true };
365
285
  }
366
286
 
367
287
  const targetDir = harnessTargetDir(harness, home);
368
288
  if (!targetDir) {
369
289
  return { expected, found: [], missing: [...expected], ok: false };
370
290
  }
371
- return walkCommandsDir(targetDir, expected);
291
+ return walkSkillAdapterDir(targetDir, routes);
372
292
  }
373
293
 
374
- function walkCommandsDir(dir, expected) {
294
+ function walkSkillAdapterDir(dir, routes) {
375
295
  if (!existsSync(dir)) {
296
+ const expected = routes.map((route) => route.slashName);
376
297
  return { expected, found: [], missing: [...expected], ok: expected.length === 0 };
377
298
  }
378
- const present = new Set();
379
- try {
380
- for (const entry of readdirSync(dir)) {
381
- if (extname(entry).toLowerCase() !== '.md') continue;
382
- const name = basename(entry, '.md');
383
- if (AW_STAGE_COMMANDS.has(name)) present.add(name);
384
- }
385
- } catch {
386
- // Unreadable directory: treat as nothing-found.
299
+
300
+ const found = [];
301
+ const missing = [];
302
+ for (const route of routes) {
303
+ const linkPath = join(dir, `${route.slashName}.md`);
304
+ if (isCorrectSymlink(linkPath, route.sourcePath)) found.push(route.slashName);
305
+ else missing.push(route.slashName);
387
306
  }
388
- const found = expected.filter((n) => present.has(n));
389
- const missing = expected.filter((n) => !present.has(n));
307
+
308
+ const expected = routes.map((route) => route.slashName);
390
309
  return { expected, found, missing, ok: missing.length === 0 };
391
310
  }
311
+
312
+ export const __test__ = {
313
+ awSkillsDir,
314
+ discoverAwSkillRoutes,
315
+ slashNameForSkill,
316
+ };
@@ -4,12 +4,12 @@
4
4
  * Why: Cursor Cloud Agent's chat UI does not pre-expand `/aw:<NAME>` slash
5
5
  * commands the way Cursor Desktop's plugin does. The model sees the raw
6
6
  * literal `/aw:...` and falls through to natural-language interpretation,
7
- * losing the contract from the matching command file even though the file
8
- * is correctly installed on disk by `commandSurface.mjs`.
7
+ * losing the contract from the matching AW skill even though the slash
8
+ * adapter is correctly installed on disk by `commandSurface.mjs`.
9
9
  *
10
10
  * Workaround: write `<repoRoot>/.cursor/rules/aw-slash-expand.mdc` — a
11
11
  * Cursor project rule that teaches the MODEL itself to read the matching
12
- * command file and execute its contract verbatim. Cursor's `.mdc` rule
12
+ * AW skill file and execute its contract verbatim. Cursor's `.mdc` rule
13
13
  * format is honored in Agent mode (verified via Cursor docs); the rule
14
14
  * loads on every session when `alwaysApply: true`.
15
15
  *
@@ -47,56 +47,35 @@ const CURSOR_RULES_SUBPATH = '.cursor/rules';
47
47
  * via npm — no additional asset bundling needed.
48
48
  */
49
49
  export const CURSOR_SLASH_SHIM_RULE = `---
50
- description: Expand \`/aw:<NAME>\` user messages by reading the matching command file
50
+ description: Expand \`/aw:<NAME>\` user messages by reading the matching AW skill file
51
51
  alwaysApply: true
52
52
  ---
53
53
 
54
- # AW slash-command expansion (Cursor Cloud workaround)
54
+ # AW skill slash expansion (Cursor Cloud workaround)
55
55
 
56
56
  When the user message starts with \`/aw:<NAME>\` (case-sensitive; \`<NAME>\` may
57
- contain hyphens both in the namespace path and in the command name), do
58
- the following BEFORE producing any other response.
57
+ contain hyphens), do the following BEFORE producing any other response.
59
58
 
60
- ## 1. Stage-command lookup (primary)
59
+ ## 1. Skill adapter lookup (primary)
61
60
 
62
61
  Read \`~/.cursor/commands/aw/<NAME>.md\`. If the file exists, jump to step 3
63
62
  with its content as the resolved instructions.
64
63
 
65
- ## 2. Registry-command lookup (fallback, hyphen-disambiguation)
64
+ ## 2. Registry skill lookup (fallback)
66
65
 
67
- If step 1 did not resolve and \`<NAME>\` contains at least one hyphen, the
68
- slash form is a flattened \`<ns_segments...>-<cmd_segments...>\` token. The
69
- boundary between namespace and command is ambiguous because both can
70
- contain hyphens (registry has e.g. \`pr-review\`, \`capture-learning\`,
71
- \`ship-auto\`, \`draw-architecture\`).
72
-
73
- Disambiguate by **iterating every hyphen position in \`<NAME>\` from
74
- RIGHTMOST to LEFTMOST**, and use the first candidate file that exists. The
66
+ If step 1 did not resolve, read the first candidate below that exists. The
75
67
  repo registry root is \`.aw/.aw_registry/\` (relative to the agent's CWD —
76
68
  the repo root in Cursor Cloud).
77
69
 
78
- For each split position \`i\` from rightmost to leftmost:
79
- - \`prefix = <NAME>[:i]\` (everything before the hyphen at position \`i\`)
80
- - \`suffix = <NAME>[i+1:]\` (everything after; may itself contain hyphens)
81
- - candidate = \`.aw/.aw_registry/<prefix-with-hyphens-replaced-by-slashes>/commands/<suffix>.md\`
82
- - Try \`Read\` on the candidate. If the file exists, jump to step 3 with
83
- that content.
84
-
85
- ### Worked examples
86
-
87
- | User typed | Registry file | Splits tried (rightmost first) |
88
- |----------------------------------------------------|------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------|
89
- | \`/aw:platform-core-brainstorm\` | \`platform/core/commands/brainstorm.md\` | \`platform/core/commands/brainstorm.md\` ✓ (first try) |
90
- | \`/aw:platform-core-pr-review\` | \`platform/core/commands/pr-review.md\` | \`platform/core/pr/commands/review.md\` ❌ → \`platform/core/commands/pr-review.md\` ✓ |
91
- | \`/aw:platform-core-capture-learning\` | \`platform/core/commands/capture-learning.md\` | \`platform/core/capture/commands/learning.md\` ❌ → \`platform/core/commands/capture-learning.md\` ✓ |
92
- | \`/aw:platform-data-clickhouse-cluster-creation\` | \`platform/data/clickhouse/commands/cluster-creation.md\` | \`…/cluster/commands/creation.md\` ❌ → \`platform/data/clickhouse/commands/cluster-creation.md\` ✓ |
70
+ 1. \`.aw/.aw_registry/aw/skills/aw-<NAME>/SKILL.md\`
71
+ 2. \`.aw/.aw_registry/aw/skills/<NAME>/SKILL.md\`
93
72
 
94
73
  ## 3. Execute
95
74
 
96
75
  Treat the resolved file's full content as your operating instructions for
97
- this turn. Execute the command's phased contract on the text that follows
98
- the slash command in the user message. Do not summarize or paraphrase the
99
- command file — execute it verbatim, including any "ask one question at a
76
+ this turn. Execute the skill's contract on the text that follows the slash
77
+ command in the user message. Do not summarize or paraphrase the skill file
78
+ — execute it verbatim, including any "ask one question at a
100
79
  time and wait" interaction protocol.
101
80
 
102
81
  ## 4. No match
@@ -104,11 +83,10 @@ time and wait" interaction protocol.
104
83
  If neither lookup resolves to an existing file, do NOT silently fall back
105
84
  to natural-language interpretation. Reply with exactly:
106
85
 
107
- > \`/aw:<NAME>\` is not a registered AW command on this machine. Run
86
+ > \`/aw:<NAME>\` is not a registered AW skill on this machine. Run
108
87
  > \`aw c4 --diagnose\` and check the output of
109
- > \`ls ~/.cursor/commands/aw/\` for available stage commands and
110
- > \`find .aw/.aw_registry -path '*/commands/*.md'\` for available registry
111
- > commands.
88
+ > \`ls ~/.cursor/commands/aw/\` for available slash skill adapters and
89
+ > \`find .aw/.aw_registry/aw/skills -name SKILL.md\` for available AW skills.
112
90
 
113
91
  This precise reply text is the smoke-test fingerprint for the rule itself.
114
92
  The string is unique enough that it cannot be confused with the model's
@@ -117,8 +95,8 @@ natural-language fallback.
117
95
  ---
118
96
 
119
97
  This rule exists because Cursor Cloud's chat UI does not pre-expand slash
120
- commands the way Cursor Desktop does. The command files ARE installed on
121
- disk by \`aw c4\`; this rule teaches the model to load them itself. See
98
+ commands the way Cursor Desktop does. The skill adapter files ARE installed
99
+ on disk by \`aw c4\`; this rule teaches the model to load them itself. See
122
100
  \`.aw_docs/features/aw-c4-cursor-slash-shim/overview.md\` for context. Do
123
101
  not edit this file by hand — \`aw c4 --harness cursor-cloud\` regenerates
124
102
  it on every run.
package/c4/index.mjs CHANGED
@@ -48,7 +48,7 @@ export {
48
48
  export { MCP_URL_DEFAULT, registerGhlAiMcp } from './mcpServer.mjs';
49
49
  export { probeMcpServer } from './mcpSmokeProbe.mjs';
50
50
  export { ensureClaudeMarketplace } from './claudePluginRegistry.mjs';
51
- export { ensureCommandSurface, ensureRegistryCommandSurface, diagnoseCommandResolution } from './commandSurface.mjs';
51
+ export { ensureCommandSurface, diagnoseCommandResolution } from './commandSurface.mjs';
52
52
  export { installCursorSlashShim, CURSOR_SLASH_SHIM_RULE } from './cursorRulesShim.mjs';
53
53
  export { ensureRepoLocalClaudeSettings } from './repoLocalClaudeSettings.mjs';
54
54
  export { copyRepoRootInstructions } from './repoRootInstructions.mjs';
@@ -129,30 +129,30 @@ aw_c4_exit=$?
129
129
 
130
130
  # Defense-in-depth: `aw c4` delegates registry sync to `aw init --silent`,
131
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
132
+ # that happens, the registry (~/.aw/.aw_registry) may be empty or incomplete.
133
+ # Re-run `aw pull` post-c4 if the AW skill count looks low.
134
+ verify_registry_skills() {
135
+ local aw_registry_dir="$HOME/.aw/.aw_registry"
136
+ local aw_skills_dir="$aw_registry_dir/aw/skills"
137
+ if [ ! -d "$aw_registry_dir" ]; then
138
138
  echo "[aw-c4-bootstrap] registry dir missing — running aw init + pull"
139
139
  aw init --no-integrations --silent 2>&1 | tail -3 || true
140
140
  aw pull 2>&1 | tail -5 || true
141
141
  return
142
142
  fi
143
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"
144
+ local skill_count
145
+ skill_count=$( (find "$aw_skills_dir" -name 'SKILL.md' 2>/dev/null || true) | wc -l | tr -d ' ' )
146
+ if [ "${skill_count:-0}" -lt 5 ]; then
147
+ echo "[aw-c4-bootstrap] only ${skill_count} AW registry skills found — running aw pull"
148
148
  aw pull 2>&1 | tail -5 || true
149
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}"
150
+ new_count=$( (find "$aw_skills_dir" -name 'SKILL.md' 2>/dev/null || true) | wc -l | tr -d ' ' )
151
+ echo "[aw-c4-bootstrap] AW registry skills: ${skill_count} -> ${new_count}"
152
152
  else
153
- echo "[aw-c4-bootstrap] registry: ${cmd_count} commands OK"
153
+ echo "[aw-c4-bootstrap] registry: ${skill_count} AW skills OK"
154
154
  fi
155
155
  }
156
- verify_registry_commands || true
156
+ verify_registry_skills || true
157
157
 
158
158
  exit "$aw_c4_exit"
package/commands/c4.mjs CHANGED
@@ -192,7 +192,7 @@ function runSelfTests({ harness, c4, home, awHome, eccHome }) {
192
192
  if (!skill.ok) failures.push('skill-resolution');
193
193
 
194
194
  // diagnoseCommandResolution is warn-only — does NOT add to failures.
195
- const command = c4.diagnoseCommandResolution({ harness, home });
195
+ const command = c4.diagnoseCommandResolution({ harness, home, awHome });
196
196
 
197
197
  let view;
198
198
  let injector;
@@ -415,7 +415,22 @@ export async function c4Command(rawArgs, overrides = {}) {
415
415
  writer.stderr('[aw-c4] registry has no namespace directories after init — retrying with aw pull\n');
416
416
  const pullRes = spawnSync('aw', ['pull'], { stdio: 'pipe' });
417
417
  if (pullRes?.status !== 0 || safeListNamespaceDirs(awRegistry, fs).length === 0) {
418
- writer.stderr('[aw-c4] FATAL: registry commands were not fetched\n');
418
+ writer.stderr('[aw-c4] FATAL: registry skills were not fetched\n');
419
+ return exit(1);
420
+ }
421
+ }
422
+ }
423
+
424
+ // Step 9b — verify active AW skills exist. `/aw:*` adapters point directly
425
+ // at ~/.aw/.aw_registry/aw/skills/<skill>/SKILL.md, so an empty registry is
426
+ // not enough; the AW namespace must be present.
427
+ {
428
+ const awSkillsDir = join(awRegistry, 'aw', 'skills');
429
+ if (!fs.existsSync(awSkillsDir)) {
430
+ writer.stderr('[aw-c4] AW skills missing after init — retrying with aw pull\n');
431
+ const pullRes = spawnSync('aw', ['pull'], { stdio: 'pipe' });
432
+ if (pullRes?.status !== 0 || !fs.existsSync(awSkillsDir)) {
433
+ writer.stderr('[aw-c4] FATAL: AW skills were not fetched\n');
419
434
  return exit(1);
420
435
  }
421
436
  }
@@ -438,26 +453,9 @@ export async function c4Command(rawArgs, overrides = {}) {
438
453
  writer.stdout('[aw-c4] MCP disabled; skipping registerGhlAiMcp\n');
439
454
  }
440
455
 
441
- // Step 12 — slash command surface (10 stage commands from ECC).
442
- safe('ensureCommandSurface', () => c4.ensureCommandSurface({ harness, home, eccHome }), writer);
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
- }
456
+ // Step 12 — slash surface backed by active AW skills. No ECC command files
457
+ // are linked; each adapter points directly at a registry SKILL.md.
458
+ safe('ensureCommandSurface', () => c4.ensureCommandSurface({ harness, home, awHome }), writer);
461
459
 
462
460
  // Step 12b — Cursor Cloud slash-expand rule (no-op on other harnesses).
463
461
  // The model-side workaround for Cursor Cloud's chat UI not pre-expanding
@@ -523,22 +523,14 @@ function findBrokenRuleReferences(filePaths) {
523
523
  return broken;
524
524
  }
525
525
 
526
- function missingCorePromptFiles(promptsDir) {
527
- return EXPECTED_AW_ROUTES
528
- .map(route => `aw-${route}.md`)
529
- .filter(fileName => !existsSync(join(promptsDir, fileName)));
530
- }
531
-
532
- function missingCoreCursorCommandFiles(commandsDir) {
533
- return EXPECTED_AW_ROUTES
534
- .map(route => `${route}.md`)
535
- .filter(fileName => !existsSync(join(commandsDir, fileName)));
536
- }
537
-
538
- function missingCoreClaudeCommandFiles(pluginRoot) {
539
- return EXPECTED_AW_ROUTES
540
- .map(route => `commands/${route}.md`)
541
- .filter(relativePath => !existsSync(join(pluginRoot, relativePath)));
526
+ function missingCoreAwSkillFiles(awRegistryDir) {
527
+ const expectedSkills = [
528
+ 'using-aw-skills',
529
+ ...EXPECTED_AW_ROUTES.map(route => `aw-${route}`),
530
+ ];
531
+ return expectedSkills
532
+ .map(skillName => `aw/skills/${skillName}/SKILL.md`)
533
+ .filter(relativePath => !awRegistryDir || !existsSync(join(awRegistryDir, relativePath)));
542
534
  }
543
535
 
544
536
  function buildDoctorChecks(homeDir, cwd) {
@@ -586,6 +578,19 @@ function buildDoctorChecks(homeDir, cwd) {
586
578
  );
587
579
  }
588
580
 
581
+ const missingAwSkills = missingCoreAwSkillFiles(awRegistryDir);
582
+ checks.push(
583
+ missingAwSkills.length === 0
584
+ ? makeCheck('aw-skills-source', 'AW skills source', 'pass', 'AW core route skills are synced under ~/.aw/.aw_registry/aw/skills')
585
+ : makeCheck(
586
+ 'aw-skills-source',
587
+ 'AW skills source',
588
+ 'fail',
589
+ `AW registry is missing core skill files: ${missingAwSkills.join(', ')}`,
590
+ 'Run `aw init` or `aw pull platform` to sync AW skills into ~/.aw/.aw_registry/aw/skills.',
591
+ ),
592
+ );
593
+
589
594
  if (cwd !== homeDir) {
590
595
  const projectAgentsPath = join(cwd, 'AGENTS.md');
591
596
  const projectClaudePath = join(cwd, 'CLAUDE.md');
@@ -646,12 +651,10 @@ function buildDoctorChecks(homeDir, cwd) {
646
651
  const missingBundleFiles = missingFiles(claudePluginRoot, [
647
652
  '.claude-plugin/plugin.json',
648
653
  'hooks/hooks.json',
649
- 'skills/using-aw-skills/SKILL.md',
650
- 'skills/using-aw-skills/hooks/session-start.sh',
651
654
  ]);
652
655
  checks.push(
653
656
  missingBundleFiles.length === 0 && claudeSessionStartStatus.ok
654
- ? makeCheck('claude-plugin-bundle', 'Claude plugin bundle', 'pass', 'Claude plugin bundle contains AW routing hooks and router skill files')
657
+ ? makeCheck('claude-plugin-bundle', 'Claude plugin bundle', 'pass', 'Claude plugin bundle contains AW routing hooks')
655
658
  : makeCheck(
656
659
  'claude-plugin-bundle',
657
660
  'Claude plugin bundle',
@@ -667,21 +670,8 @@ function buildDoctorChecks(homeDir, cwd) {
667
670
  ),
668
671
  );
669
672
 
670
- const missingPluginCommands = missingCoreClaudeCommandFiles(claudePluginRoot);
671
- checks.push(
672
- missingPluginCommands.length === 0
673
- ? makeCheck('claude-plugin-commands', 'Claude public commands', 'pass', 'Claude plugin bundle exposes the current AW command surface (primary, conditional, and compatibility routes)')
674
- : makeCheck(
675
- 'claude-plugin-commands',
676
- 'Claude public commands',
677
- 'fail',
678
- `Claude plugin bundle is missing core command files: ${missingPluginCommands.join(', ')}`,
679
- 'Refresh the AW Claude plugin so the plugin bundle includes the full AW public command surface.',
680
- ),
681
- );
682
673
  } else {
683
674
  checks.push(makeCheck('claude-plugin-bundle', 'Claude plugin bundle', 'warn', 'Claude plugin bundle is not installed', 'Enable/install the AW Claude plugin, then rerun `aw doctor`.'));
684
- checks.push(makeCheck('claude-plugin-commands', 'Claude public commands', 'warn', 'Claude plugin command bundle could not be inspected because the plugin is not installed'));
685
675
  }
686
676
 
687
677
  const claudeLegacyHooks = parseLegacyClaudeHookTargets(claudeSettings?.hooks?.SessionStart || []);
@@ -867,19 +857,6 @@ function buildDoctorChecks(homeDir, cwd) {
867
857
  : makeCheck('codex-references', 'Codex shared references', 'fail', 'Codex shared references are missing', projectRelinkFix(homeDir, cwd, '~/.codex/references')),
868
858
  );
869
859
 
870
- const missingPrompts = missingCorePromptFiles(join(homeDir, '.codex', 'prompts'));
871
- checks.push(
872
- missingPrompts.length === 0
873
- ? makeCheck('codex-prompts', 'Codex prompts', 'pass', 'Codex prompt sync produced the current AW prompt surface (primary, conditional, and compatibility routes)')
874
- : makeCheck(
875
- 'codex-prompts',
876
- 'Codex prompts',
877
- 'fail',
878
- `Codex is missing core prompt files: ${missingPrompts.join(', ')}`,
879
- 'Run `aw init` or refresh the AW ECC bundle to regenerate the Codex prompts.',
880
- ),
881
- );
882
-
883
860
  const codexAgentsPath = join(homeDir, '.codex', 'AGENTS.md');
884
861
  checks.push(
885
862
  existsSync(codexAgentsPath) && textHasManagedRouterBridge(readText(codexAgentsPath)) && textHasRulesReference(readText(codexAgentsPath))
@@ -944,20 +921,6 @@ function buildDoctorChecks(homeDir, cwd) {
944
921
  : makeCheck('cursor-install-state', 'Cursor install state', 'fail', 'Cursor install-state file is missing', globalInstallStateFix(homeDir, cwd, 'Cursor install state')),
945
922
  );
946
923
 
947
- const cursorCommandsDir = join(homeDir, '.cursor', 'commands', 'aw');
948
- const missingCursorCommands = missingCoreCursorCommandFiles(cursorCommandsDir);
949
- checks.push(
950
- missingCursorCommands.length === 0
951
- ? makeCheck('cursor-commands', 'Cursor public commands', 'pass', 'Cursor has the current AW command surface under ~/.cursor/commands/aw/')
952
- : makeCheck(
953
- 'cursor-commands',
954
- 'Cursor public commands',
955
- 'fail',
956
- `Cursor is missing core command files: ${missingCursorCommands.join(', ')}`,
957
- projectRelinkFix(homeDir, cwd, 'AW command files under ~/.cursor/commands/aw/'),
958
- ),
959
- );
960
-
961
924
  const cursorMcp = jsonMcpHealth(join(homeDir, '.cursor', 'mcp.json'));
962
925
  checks.push(
963
926
  cursorMcp.present && cursorMcp.url && cursorMcp.authorization
package/ecc.mjs CHANGED
@@ -3,7 +3,7 @@ import { promisify } from "node:util";
3
3
  const execAsync = promisify(execCb);
4
4
  import {
5
5
  existsSync, readFileSync, readdirSync,
6
- mkdirSync, rmSync, writeFileSync, renameSync,
6
+ mkdirSync, rmSync, writeFileSync,
7
7
  } from "node:fs";
8
8
  import { dirname, join } from "node:path";
9
9
  import { homedir } from "node:os";
@@ -16,7 +16,6 @@ export const AW_ECC_TAG = "v1.4.66";
16
16
  const REQUIRED_ECC_FILES = [
17
17
  "package.json",
18
18
  "scripts/install-apply.js",
19
- "scripts/sync-ecc-to-codex.sh",
20
19
  ];
21
20
 
22
21
  const MARKETPLACE_NAME = "aw-marketplace";
@@ -24,11 +23,10 @@ const PLUGIN_KEY = `aw@${MARKETPLACE_NAME}`;
24
23
 
25
24
  function eccDir() { return join(homedir(), ".aw-ecc"); }
26
25
 
27
- // File-copy targets use explicit non-skill module sets so hooks, rules,
28
- // command compatibility, shared references, and install-state can remain
29
- // available while AW skills come from platform-docs/.aw_registry/aw.
30
- // Using `--without baseline:commands` with broader profiles is unsafe because
31
- // some optional modules depend on commands-core and cause install-apply to fail.
26
+ // File-copy targets use explicit non-skill, non-command module sets so hooks,
27
+ // rules, shared references, and install-state can remain available while AW
28
+ // skills come from platform-docs/.aw_registry/aw. Slash access is backed by
29
+ // those skills directly, so aw-ecc command artifacts are intentionally skipped.
32
30
  const FILE_COPY_TARGETS = ["claude", "cursor", "codex"];
33
31
  const FILE_COPY_MODULES_BY_TARGET = {
34
32
  claude: [
@@ -40,7 +38,6 @@ const FILE_COPY_MODULES_BY_TARGET = {
40
38
  cursor: [
41
39
  "rules-core",
42
40
  "agents-core",
43
- "commands-core",
44
41
  "hooks-runtime",
45
42
  "platform-configs",
46
43
  ],
@@ -255,8 +252,8 @@ function cloneOrUpdate(tag, dest) {
255
252
 
256
253
  /**
257
254
  * Transform canonical /aw: references to Cursor-compatible /aw- in installed
258
- * skill and rule files. Cursor namespaces commands via directory structure
259
- * (commands/aw/plan.md /aw-plan) rather than colons.
255
+ * rule files. Cursor's model-side rule still sees hyphenated route mentions in
256
+ * some generated home instructions.
260
257
  */
261
258
  function transformCursorAwRefs(home) {
262
259
  const dirs = [
@@ -293,79 +290,16 @@ function uninstallClaudePlugin() {
293
290
  try { run(`claude plugin marketplace remove ${MARKETPLACE_NAME}`); } catch { /* not registered */ }
294
291
  }
295
292
 
296
- /**
297
- * Move ecc command files from ~/.cursor/commands/*.md ~/.cursor/commands/aw/*.md
298
- * so Cursor exposes them as /aw:tdd, /aw:plan consistent with Claude Code's plugin namespace.
299
- * Also updates the ecc-install-state.json paths so nuke can clean them up correctly.
300
- */
301
- function namespaceCursorCommands(home) {
302
- const commandsDir = join(home, ".cursor", "commands");
303
- const awDir = join(commandsDir, "aw");
304
- if (!existsSync(commandsDir)) return;
305
-
306
- // Move any flat .md files (not already in a subdirectory) into aw/
307
- const moved = [];
308
- for (const file of readdirSync(commandsDir)) {
309
- if (!file.endsWith(".md") || file.startsWith(".")) continue;
310
- const src = join(commandsDir, file);
311
- mkdirSync(awDir, { recursive: true });
312
- const dest = join(awDir, file);
293
+ function pruneEccAuthoredSkillsAndCommands(repoDir) {
294
+ // The local aw-ecc clone is now only a compatibility runtime for hooks,
295
+ // rules, agents, and configs. Active AW skills live in platform-docs under
296
+ // ~/.aw/.aw_registry/aw/skills, and slash adapters point at those SKILL.md
297
+ // files directly.
298
+ for (const relPath of ["commands", "skills"]) {
313
299
  try {
314
- renameSync(src, dest);
315
- moved.push({ from: src, to: dest });
300
+ rmSync(join(repoDir, relPath), { recursive: true, force: true });
316
301
  } catch { /* best effort */ }
317
302
  }
318
-
319
- if (moved.length === 0) return;
320
-
321
- // Update install-state so nuke removes files from the new aw/ location
322
- const statePath = join(home, ".cursor", "ecc-install-state.json");
323
- if (!existsSync(statePath)) return;
324
- try {
325
- const state = JSON.parse(readFileSync(statePath, "utf8"));
326
- const pathMap = new Map(moved.map(({ from, to }) => [from, to]));
327
- state.operations = (state.operations || []).map((op) => {
328
- const newPath = op.destinationPath && pathMap.get(op.destinationPath);
329
- return newPath ? { ...op, destinationPath: newPath } : op;
330
- });
331
- writeFileSync(statePath, JSON.stringify(state, null, 2));
332
- } catch { /* best effort */ }
333
- }
334
-
335
- /**
336
- * Generate Codex prompt files from ECC commands only.
337
- *
338
- * The old ECC sync script also materialized ECC skills into Codex-owned
339
- * skill directories. AW skills now come from platform-docs/.aw_registry/aw,
340
- * so this runtime keeps the command prompt compatibility surface and does
341
- * not run the broad ECC skill sync.
342
- */
343
- function syncEccCommandsToCodex(repoDir) {
344
- const commandsDir = join(repoDir, "commands");
345
- if (!existsSync(commandsDir)) return;
346
- try {
347
- const promptsDir = join(homedir(), ".codex", "prompts");
348
- mkdirSync(promptsDir, { recursive: true });
349
- const manifest = [];
350
-
351
- for (const file of readdirSync(commandsDir).filter((entry) => entry.endsWith(".md") && !entry.startsWith("."))) {
352
- const commandPath = join(commandsDir, file);
353
- const content = readFileSync(commandPath, "utf8");
354
- const basename = file.replace(/\.md$/, "");
355
- const heading = content.match(/^#\s+(\/\S+)/m)?.[1];
356
- const promptName = heading?.startsWith("/aw:")
357
- ? heading.slice(1).replace(/:/g, "-").replace(/[^\w.-]/g, "-")
358
- : `ecc-${basename}`;
359
- const promptFile = `${promptName}.md`;
360
- writeFileSync(join(promptsDir, promptFile), content);
361
- manifest.push(promptFile);
362
- }
363
-
364
- writeFileSync(
365
- join(promptsDir, "ecc-prompts-manifest.txt"),
366
- manifest.sort().map((entry) => `${entry}\n`).join(""),
367
- );
368
- } catch { /* best effort — codex sync failure is non-blocking */ }
369
303
  }
370
304
 
371
305
  export async function installAwEcc(
@@ -382,6 +316,7 @@ export async function installAwEcc(
382
316
  try {
383
317
  if (eccSpinner) eccSpinner.start('Cloning aw-ecc engine...');
384
318
  await cloneOrUpdateAsync(AW_ECC_TAG, repoDir);
319
+ pruneEccAuthoredSkillsAndCommands(repoDir);
385
320
  if (eccSpinner) eccSpinner.message('Installing aw-ecc dependencies...');
386
321
 
387
322
  // Claude Code: plugin install via marketplace CLI (proper agent dispatch)
@@ -425,12 +360,8 @@ export async function installAwEcc(
425
360
  { cwd: runCwd },
426
361
  );
427
362
  if (target === "cursor") {
428
- namespaceCursorCommands(runCwd);
429
363
  transformCursorAwRefs(home);
430
364
  }
431
- if (target === "codex") {
432
- syncEccCommandsToCodex(repoDir);
433
- }
434
365
  restoreProtectedConfigs(snapshot);
435
366
  } catch { /* target not supported — skip */ }
436
367
  }));
@@ -482,8 +413,8 @@ export function uninstallAwEcc({ silent = false } = {}) {
482
413
  } catch { /* corrupted state — skip */ }
483
414
  }
484
415
 
485
- // Codex: remove generated prompt files from sync-ecc-to-codex.sh
486
- // (not tracked in install-state — cleaned via manifests, with prefix fallback)
416
+ // Codex: remove legacy generated prompt files from older ECC sync flows
417
+ // (not tracked in install-state — cleaned via manifests, with prefix fallback).
487
418
  const codexPromptsDir = join(HOME, ".codex", "prompts");
488
419
  if (existsSync(codexPromptsDir)) {
489
420
  try {
package/integrate.mjs CHANGED
@@ -1,10 +1,9 @@
1
1
  // integrate.mjs — Generate commands for all IDEs, instructions (CLAUDE.md, AGENTS.md)
2
2
 
3
- import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, rmSync } from 'node:fs';
3
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'node:fs';
4
4
  import { dirname, join } from 'node:path';
5
5
  import { homedir } from 'node:os';
6
6
  import * as fmt from './fmt.mjs';
7
- import * as config from './config.mjs';
8
7
  import { getLocalRegistryDir } from './git.mjs';
9
8
  import {
10
9
  generateAgentsMdRulesSection,
@@ -174,8 +173,8 @@ function applyManagedInstructionSections(content, file, rulesSections = {}, opti
174
173
  }
175
174
 
176
175
  /**
177
- * Count hand-written commands already present in the registry.
178
- * No CLI stub generation all commands come from the registry itself.
176
+ * Command slash adapters are deprecated. Keep this function as a compatibility
177
+ * no-op for older call sites and clean the removed generated-command folder.
179
178
  */
180
179
  export function generateCommands(cwd, { silent = false } = {}) {
181
180
  const awDir = getLocalRegistryDir(cwd, join(homedir(), '.aw_registry'));
@@ -184,47 +183,8 @@ export function generateCommands(cwd, { silent = false } = {}) {
184
183
  const oldGenDir = join(awDir, '.generated-commands');
185
184
  if (existsSync(oldGenDir)) rmSync(oldGenDir, { recursive: true, force: true });
186
185
 
187
- // Count hand-written commands across all namespaces for reporting
188
- let count = 0;
189
- const namespaces = getTeamNamespaces(awDir);
190
- for (const ns of namespaces) {
191
- const nsDir = join(awDir, ns);
192
- if (existsSync(nsDir)) {
193
- const cmdFiles = findFiles(nsDir, 'commands');
194
- count += cmdFiles.length;
195
- }
196
- }
197
-
198
- if (count > 0 && !silent) {
199
- fmt.logSuccess(`Generated ${count} aw commands`);
200
- }
201
-
202
- return count;
203
- }
204
-
205
- function findFiles(dir, typeName) {
206
- const results = [];
207
- function walk(d) {
208
- let entries;
209
- try {
210
- entries = readdirSync(d, { withFileTypes: true });
211
- } catch { return; } // not a directory — skip gracefully
212
- for (const entry of entries) {
213
- if (entry.name.startsWith('.')) continue;
214
- const full = join(d, entry.name);
215
- if (entry.isDirectory()) {
216
- if (entry.name === typeName) {
217
- for (const f of readdirSync(full)) {
218
- if (f.endsWith('.md')) results.push(join(full, f));
219
- }
220
- } else {
221
- walk(full);
222
- }
223
- }
224
- }
225
- }
226
- walk(dir);
227
- return results;
186
+ if (!silent) fmt.logSuccess('Command adapters are disabled; use registry-backed AW skills');
187
+ return 0;
228
188
  }
229
189
 
230
190
  /**
@@ -711,19 +671,3 @@ No active tasks. Tasks are created during workflow execution.
711
671
  fmt.logSuccess('Orchestration state ready');
712
672
  }
713
673
  }
714
-
715
- /**
716
- * Return top-level team namespace names from config (excludes 'platform').
717
- * cfg.include may contain full paths like 'mobile/core/backend/agents/dev.md'
718
- * so we extract only the first path segment (the actual namespace folder).
719
- * E.g. ['mobile/core/backend/agents/dev.md', 'revex'] → ['mobile', 'revex']
720
- */
721
- function getTeamNamespaces(awDir) {
722
- const cfg = config.load(awDir);
723
- if (!cfg || !cfg.include) return [];
724
- return [...new Set(
725
- cfg.include
726
- .filter(p => p !== 'platform')
727
- .map(p => p.split('/')[0]),
728
- )];
729
- }
package/link.mjs CHANGED
@@ -1,9 +1,10 @@
1
1
  // link.mjs — Create symlinks from IDE dirs → .aw_registry/
2
2
 
3
- import { existsSync, lstatSync, mkdirSync, readdirSync, unlinkSync, symlinkSync, rmdirSync, realpathSync } from 'node:fs';
4
- import { join, relative } from 'node:path';
3
+ import { existsSync, lstatSync, mkdirSync, readdirSync, unlinkSync, symlinkSync, rmdirSync, realpathSync, rmSync } from 'node:fs';
4
+ import { dirname, join, relative } from 'node:path';
5
5
  import { homedir } from 'node:os';
6
6
  import * as fmt from './fmt.mjs';
7
+ import { ensureCommandSurface } from './c4/commandSurface.mjs';
7
8
  import { getLocalRegistryDir } from './git.mjs';
8
9
 
9
10
  function forceSymlink(target, linkPath) {
@@ -130,6 +131,31 @@ function cleanIdeSymlinks(cwd) {
130
131
  }
131
132
  }
132
133
 
134
+ function pruneManagedAwCommandNamespaces(cwd) {
135
+ let pruned = 0;
136
+ for (const ide of IDE_DIRS) {
137
+ const commandDir = join(cwd, ide, 'commands', 'aw');
138
+ if (!existsSync(commandDir)) continue;
139
+ try {
140
+ rmSync(commandDir, { recursive: true, force: true });
141
+ pruned++;
142
+ } catch {
143
+ // Best effort: stale command cleanup should not block registry linking.
144
+ }
145
+ }
146
+ return pruned;
147
+ }
148
+
149
+ function ensureSkillSlashAdapters(cwd, awDir) {
150
+ const awHome = dirname(awDir);
151
+ let linked = 0;
152
+ for (const harness of ['cursor-cloud', 'codex-web']) {
153
+ const result = ensureCommandSurface({ harness, home: cwd, awHome });
154
+ linked += result.found.length;
155
+ }
156
+ return linked;
157
+ }
158
+
133
159
  /**
134
160
  * Remove all symlinks in a directory, then prune empty subdirectories.
135
161
  * Walks depth-first so children are cleaned before parents.
@@ -196,6 +222,9 @@ export function linkWorkspace(cwd, awDirOverride = null, { silent = false } = {}
196
222
 
197
223
  // Clean old symlinks first
198
224
  cleanIdeSymlinks(cwd);
225
+ if (cwd === HOME) {
226
+ pruneManagedAwCommandNamespaces(cwd);
227
+ }
199
228
 
200
229
  const namespaces = listNamespaceDirs(awDir);
201
230
 
@@ -344,22 +373,10 @@ export function linkWorkspace(cwd, awDirOverride = null, { silent = false } = {}
344
373
  }
345
374
  }
346
375
 
347
- // Commands: per-file symlinks (recursive for nested domain dirs)
348
- for (const ns of namespaces) {
349
- for (const { typeDirPath: commandsDir, segments } of findNestedTypeDirs(join(awDir, ns), 'commands')) {
350
- for (const file of readdirSync(commandsDir).filter(f => f.endsWith('.md') && !f.startsWith('.'))) {
351
- const cmdFileName = flatRegistryName(ns, ...segments, file);
352
-
353
- for (const ide of IDE_DIRS) {
354
- const linkDir = join(cwd, ide, 'commands', 'aw');
355
- mkdirSync(linkDir, { recursive: true });
356
- const linkPath = join(linkDir, cmdFileName);
357
- const targetPath = join(commandsDir, file);
358
- const relTarget = relative(linkDir, targetPath);
359
- try { forceSymlink(relTarget, linkPath); created++; } catch { /* best effort */ }
360
- }
361
- }
362
- }
376
+ // Slash adapters are skills-only. Keep the harness discovery paths populated,
377
+ // but point every /aw:* file at .aw_registry/aw/skills/*/SKILL.md.
378
+ if (cwd === HOME) {
379
+ try { created += ensureSkillSlashAdapters(cwd, awDir); } catch { /* best effort */ }
363
380
  }
364
381
 
365
382
  // AW-PROTOCOL.md: symlink into each IDE dir root so commands can always find it
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghl-ai/aw",
3
- "version": "0.1.70-beta.0",
3
+ "version": "0.1.70-beta.2",
4
4
  "description": "Agentic Workspace CLI — pull, push & manage agents, skills and commands from the registry",
5
5
  "type": "module",
6
6
  "bin": {