@ghl-ai/aw 0.1.69 → 0.1.70-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.
@@ -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 {
@@ -36,45 +27,48 @@ import {
36
27
  symlinkSync,
37
28
  unlinkSync,
38
29
  } from 'node:fs';
39
- import { join, basename, extname, relative } from 'node:path';
30
+ import { join } from 'node:path';
31
+
32
+ function awSkillsDir(awHome) {
33
+ return join(awHome, '.aw_registry', 'aw', 'skills');
34
+ }
40
35
 
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
- ]);
36
+ function slashNameForSkill(skillName) {
37
+ return skillName.startsWith('aw-') && skillName.length > 3
38
+ ? skillName.slice(3)
39
+ : skillName;
40
+ }
57
41
 
58
42
  /**
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).
43
+ * Discover active AW skills and their slash aliases.
62
44
  *
63
- * @param {string} eccHome
64
- * @returns {string[]}
45
+ * @param {string} awHome
46
+ * @returns {Array<{ skillName: string, slashName: string, sourcePath: string }>}
65
47
  */
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);
48
+ function discoverAwSkillRoutes(awHome) {
49
+ const skillsDir = awSkillsDir(awHome);
50
+ if (!existsSync(skillsDir)) return [];
51
+
52
+ const routes = new Map();
53
+ for (const entry of safeReaddir(skillsDir)) {
54
+ if (entry.startsWith('.')) continue;
55
+ const skillDir = join(skillsDir, entry);
56
+ if (!isDir(skillDir)) continue;
57
+ const sourcePath = join(skillDir, 'SKILL.md');
58
+ if (!existsSync(sourcePath)) continue;
59
+
60
+ const slashName = slashNameForSkill(entry);
61
+ const existing = routes.get(slashName);
62
+ const candidate = { skillName: entry, slashName, sourcePath };
63
+ if (!existing || (entry.startsWith('aw-') && !existing.skillName.startsWith('aw-'))) {
64
+ routes.set(slashName, candidate);
65
+ }
75
66
  }
76
- stageNames.sort();
77
- return stageNames;
67
+
68
+ return [...routes.values()].sort((a, b) => {
69
+ const bySlash = a.slashName.localeCompare(b.slashName);
70
+ return bySlash === 0 ? a.skillName.localeCompare(b.skillName) : bySlash;
71
+ });
78
72
  }
79
73
 
80
74
  function harnessTargetDir(harness, home) {
@@ -99,29 +93,40 @@ function isCorrectSymlink(linkPath, expectedTarget) {
99
93
  }
100
94
  }
101
95
 
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.
96
+ function lstatExists(p) {
111
97
  try {
112
- if (existsSync(linkPath) || lstatExists(linkPath)) {
113
- try { unlinkSync(linkPath); } catch { /* may be a directory */ }
114
- }
115
- symlinkSync(sourcePath, linkPath);
98
+ lstatSync(p);
116
99
  return true;
117
100
  } catch {
118
101
  return false;
119
102
  }
120
103
  }
121
104
 
122
- function lstatExists(p) {
105
+ function isDir(p) {
123
106
  try {
124
- lstatSync(p);
107
+ return lstatSync(p).isDirectory();
108
+ } catch {
109
+ return false;
110
+ }
111
+ }
112
+
113
+ function safeReaddir(dir) {
114
+ try {
115
+ return readdirSync(dir);
116
+ } catch {
117
+ return [];
118
+ }
119
+ }
120
+
121
+ function linkSkillRoute(route, targetDir) {
122
+ const linkPath = join(targetDir, `${route.slashName}.md`);
123
+ if (isCorrectSymlink(linkPath, route.sourcePath)) return true;
124
+
125
+ try {
126
+ if (existsSync(linkPath) || lstatExists(linkPath)) {
127
+ try { unlinkSync(linkPath); } catch { /* may be a directory */ }
128
+ }
129
+ symlinkSync(route.sourcePath, linkPath);
125
130
  return true;
126
131
  } catch {
127
132
  return false;
@@ -129,15 +134,16 @@ function lstatExists(p) {
129
134
  }
130
135
 
131
136
  /**
132
- * Ensure the harness slash-command surface for AW stages.
137
+ * Ensure the harness slash surface for active AW skills.
133
138
  *
134
139
  * @param {object} opts
135
140
  * @param {'claude-web'|'cursor-cloud'|'codex-web'|string} opts.harness
136
141
  * @param {string} opts.home
137
- * @param {string} opts.eccHome
142
+ * @param {string} opts.awHome
138
143
  * @returns {{
139
144
  * harness: string,
140
145
  * expectedCommands: string[],
146
+ * expectedSkills: string[],
141
147
  * found: string[],
142
148
  * missing: string[],
143
149
  * installedAction: 'symlink' | 'noop' | 'unsupported',
@@ -147,23 +153,23 @@ export function ensureCommandSurface(opts) {
147
153
  if (!opts || typeof opts !== 'object') {
148
154
  throw new Error('ensureCommandSurface: opts object is required');
149
155
  }
150
- const { harness, home, eccHome } = opts;
156
+ const { harness, home, awHome } = opts;
151
157
  if (!home || typeof home !== 'string') {
152
158
  throw new Error('ensureCommandSurface: opts.home is required');
153
159
  }
154
- if (!eccHome || typeof eccHome !== 'string') {
155
- throw new Error('ensureCommandSurface: opts.eccHome is required');
160
+ if (!awHome || typeof awHome !== 'string') {
161
+ throw new Error('ensureCommandSurface: opts.awHome is required');
156
162
  }
157
163
 
158
- const expectedCommands = discoverEccStageCommands(eccHome);
164
+ const routes = discoverAwSkillRoutes(awHome);
165
+ const expectedCommands = routes.map((route) => route.slashName);
166
+ const expectedSkills = routes.map((route) => route.skillName);
159
167
 
160
168
  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
169
  return {
165
170
  harness,
166
171
  expectedCommands,
172
+ expectedSkills,
167
173
  found: [...expectedCommands],
168
174
  missing: [],
169
175
  installedAction: 'noop',
@@ -175,38 +181,31 @@ export function ensureCommandSurface(opts) {
175
181
  return {
176
182
  harness,
177
183
  expectedCommands,
184
+ expectedSkills,
178
185
  found: [],
179
186
  missing: [],
180
187
  installedAction: 'unsupported',
181
188
  };
182
189
  }
183
190
 
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
191
  try {
188
192
  mkdirSync(targetDir, { recursive: true });
189
193
  } catch {
190
194
  return {
191
195
  harness,
192
196
  expectedCommands,
197
+ expectedSkills,
193
198
  found: [],
194
199
  missing: [...expectedCommands],
195
200
  installedAction: 'symlink',
196
201
  };
197
202
  }
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) {
203
+
204
+ if (!isDir(targetDir)) {
207
205
  return {
208
206
  harness,
209
207
  expectedCommands,
208
+ expectedSkills,
210
209
  found: [],
211
210
  missing: [...expectedCommands],
212
211
  installedAction: 'symlink',
@@ -215,15 +214,16 @@ export function ensureCommandSurface(opts) {
215
214
 
216
215
  const found = [];
217
216
  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);
217
+ for (const route of routes) {
218
+ const ok = linkSkillRoute(route, targetDir);
219
+ if (ok) found.push(route.slashName);
220
+ else missing.push(route.slashName);
222
221
  }
223
222
 
224
223
  return {
225
224
  harness,
226
225
  expectedCommands,
226
+ expectedSkills,
227
227
  found,
228
228
  missing,
229
229
  installedAction: 'symlink',
@@ -231,161 +231,59 @@ export function ensureCommandSurface(opts) {
231
231
  }
232
232
 
233
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
- 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.
234
+ * Read-only diagnostic. For Cursor/Codex, verifies that slash adapter files
235
+ * point to the active AW skill `SKILL.md` files. Claude is skill/plugin
236
+ * routed and does not require adapter files.
341
237
  *
342
238
  * @param {object} opts
343
239
  * @param {'claude-web'|'cursor-cloud'|'codex-web'|string} opts.harness
344
240
  * @param {string} opts.home
241
+ * @param {string} [opts.awHome]
345
242
  * @returns {{ expected: string[], found: string[], missing: string[], ok: boolean }}
346
243
  */
347
244
  export function diagnoseCommandResolution(opts) {
348
245
  if (!opts || typeof opts !== 'object') {
349
246
  throw new Error('diagnoseCommandResolution: opts object is required');
350
247
  }
351
- const { harness, home } = opts;
248
+ const { harness, home, awHome } = opts;
352
249
  if (!home || typeof home !== 'string') {
353
250
  throw new Error('diagnoseCommandResolution: opts.home is required');
354
251
  }
355
- const expected = [...AW_STAGE_COMMANDS].sort();
252
+
253
+ const routes = awHome && typeof awHome === 'string' ? discoverAwSkillRoutes(awHome) : [];
254
+ const expected = routes.map((route) => route.slashName);
356
255
 
357
256
  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);
257
+ return { expected, found: [...expected], missing: [], ok: true };
365
258
  }
366
259
 
367
260
  const targetDir = harnessTargetDir(harness, home);
368
261
  if (!targetDir) {
369
262
  return { expected, found: [], missing: [...expected], ok: false };
370
263
  }
371
- return walkCommandsDir(targetDir, expected);
264
+ return walkSkillAdapterDir(targetDir, routes);
372
265
  }
373
266
 
374
- function walkCommandsDir(dir, expected) {
267
+ function walkSkillAdapterDir(dir, routes) {
375
268
  if (!existsSync(dir)) {
269
+ const expected = routes.map((route) => route.slashName);
376
270
  return { expected, found: [], missing: [...expected], ok: expected.length === 0 };
377
271
  }
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.
272
+
273
+ const found = [];
274
+ const missing = [];
275
+ for (const route of routes) {
276
+ const linkPath = join(dir, `${route.slashName}.md`);
277
+ if (isCorrectSymlink(linkPath, route.sourcePath)) found.push(route.slashName);
278
+ else missing.push(route.slashName);
387
279
  }
388
- const found = expected.filter((n) => present.has(n));
389
- const missing = expected.filter((n) => !present.has(n));
280
+
281
+ const expected = routes.map((route) => route.slashName);
390
282
  return { expected, found, missing, ok: missing.length === 0 };
391
283
  }
284
+
285
+ export const __test__ = {
286
+ awSkillsDir,
287
+ discoverAwSkillRoutes,
288
+ slashNameForSkill,
289
+ };
@@ -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.