@ghl-ai/aw 0.1.69 → 0.1.70-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/c4/claudePluginRegistry.mjs +3 -3
- package/c4/commandSurface.mjs +122 -224
- package/c4/cursorRulesShim.mjs +19 -41
- package/c4/diagnostics.mjs +23 -42
- package/c4/index.mjs +1 -2
- package/c4/preflight.mjs +12 -18
- package/c4/templates/scripts/aw-c4-bootstrap.sh +14 -14
- package/codex.mjs +13 -4
- package/commands/c4.mjs +31 -34
- package/commands/doctor.mjs +24 -61
- package/commands/init.mjs +12 -3
- package/commands/pull.mjs +2 -1
- package/constants.mjs +3 -0
- package/ecc.mjs +41 -74
- package/git.mjs +12 -9
- package/hooks/codex-home.mjs +3 -3
- package/link.mjs +13 -11
- package/package.json +1 -1
- package/startup.mjs +7 -22
- package/c4/eccRegistryBridge.mjs +0 -236
|
@@ -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.
|