@ghl-ai/aw 0.1.70-beta.0 → 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.
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/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.1",
4
4
  "description": "Agentic Workspace CLI — pull, push & manage agents, skills and commands from the registry",
5
5
  "type": "module",
6
6
  "bin": {