@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.
- package/c4/claudePluginRegistry.mjs +3 -3
- package/c4/commandSurface.mjs +122 -224
- package/c4/cursorRulesShim.mjs +19 -41
- package/c4/index.mjs +1 -1
- package/c4/templates/scripts/aw-c4-bootstrap.sh +14 -14
- package/commands/c4.mjs +20 -22
- package/commands/doctor.mjs +22 -59
- package/ecc.mjs +17 -86
- package/package.json +1 -1
|
@@ -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
|
|
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
|
|
118
|
-
*
|
|
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).
|
package/c4/commandSurface.mjs
CHANGED
|
@@ -1,30 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* c4/commandSurface.mjs — slash-command resolution per harness
|
|
2
|
+
* c4/commandSurface.mjs — slash-command resolution per harness.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
|
11
|
-
*
|
|
12
|
-
* ~/.
|
|
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
|
-
*
|
|
18
|
-
* -
|
|
19
|
-
*
|
|
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
|
-
* `
|
|
24
|
-
*
|
|
25
|
-
*
|
|
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
|
|
30
|
+
import { join } from 'node:path';
|
|
31
|
+
|
|
32
|
+
function awSkillsDir(awHome) {
|
|
33
|
+
return join(awHome, '.aw_registry', 'aw', 'skills');
|
|
34
|
+
}
|
|
40
35
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
*
|
|
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}
|
|
64
|
-
* @returns {string
|
|
45
|
+
* @param {string} awHome
|
|
46
|
+
* @returns {Array<{ skillName: string, slashName: string, sourcePath: string }>}
|
|
65
47
|
*/
|
|
66
|
-
function
|
|
67
|
-
const
|
|
68
|
-
if (!existsSync(
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
for (const entry of
|
|
72
|
-
if (
|
|
73
|
-
const
|
|
74
|
-
if (
|
|
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
|
-
|
|
77
|
-
return
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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,
|
|
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 (!
|
|
155
|
-
throw new Error('ensureCommandSurface: opts.
|
|
160
|
+
if (!awHome || typeof awHome !== 'string') {
|
|
161
|
+
throw new Error('ensureCommandSurface: opts.awHome is required');
|
|
156
162
|
}
|
|
157
163
|
|
|
158
|
-
const
|
|
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
|
-
|
|
199
|
-
|
|
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
|
|
219
|
-
const ok =
|
|
220
|
-
if (ok) found.push(
|
|
221
|
-
else missing.push(
|
|
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
|
-
*
|
|
235
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
264
|
+
return walkSkillAdapterDir(targetDir, routes);
|
|
372
265
|
}
|
|
373
266
|
|
|
374
|
-
function
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
|
|
389
|
-
const
|
|
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
|
+
};
|
package/c4/cursorRulesShim.mjs
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
|
58
|
-
the following BEFORE producing any other response.
|
|
57
|
+
contain hyphens), do the following BEFORE producing any other response.
|
|
59
58
|
|
|
60
|
-
## 1.
|
|
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
|
|
64
|
+
## 2. Registry skill lookup (fallback)
|
|
66
65
|
|
|
67
|
-
If step 1 did not resolve
|
|
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
|
-
|
|
79
|
-
|
|
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
|
|
98
|
-
|
|
99
|
-
|
|
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
|
|
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
|
|
110
|
-
> \`find .aw/.aw_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
|
|
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,
|
|
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
|
-
#
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
local
|
|
137
|
-
if [ ! -d "$
|
|
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
|
|
145
|
-
|
|
146
|
-
if [ "${
|
|
147
|
-
echo "[aw-c4-bootstrap] only ${
|
|
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 "$
|
|
151
|
-
echo "[aw-c4-bootstrap] registry
|
|
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: ${
|
|
153
|
+
echo "[aw-c4-bootstrap] registry: ${skill_count} AW skills OK"
|
|
154
154
|
fi
|
|
155
155
|
}
|
|
156
|
-
|
|
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
|
|
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
|
|
442
|
-
|
|
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
|
package/commands/doctor.mjs
CHANGED
|
@@ -523,22 +523,14 @@ function findBrokenRuleReferences(filePaths) {
|
|
|
523
523
|
return broken;
|
|
524
524
|
}
|
|
525
525
|
|
|
526
|
-
function
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
.
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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
|
|
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,
|
|
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,
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
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
|
-
*
|
|
259
|
-
*
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
|
|
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
|
|
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 {
|