@ghl-ai/aw 0.1.70-beta.1 → 0.1.70-beta.3
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 +224 -122
- package/c4/cursorRulesShim.mjs +41 -19
- package/c4/diagnostics.mjs +42 -23
- package/c4/eccRegistryBridge.mjs +236 -0
- package/c4/index.mjs +2 -1
- package/c4/preflight.mjs +18 -12
- package/c4/templates/scripts/aw-c4-bootstrap.sh +14 -14
- package/codex.mjs +4 -13
- package/commands/c4.mjs +34 -31
- package/commands/doctor.mjs +61 -24
- package/commands/init.mjs +14 -20
- package/commands/pull.mjs +1 -2
- package/constants.mjs +0 -3
- package/ecc.mjs +74 -41
- package/git.mjs +9 -12
- package/hooks/codex-home.mjs +3 -3
- package/integrations.mjs +28 -3
- package/link.mjs +11 -13
- package/package.json +1 -1
- package/startup.mjs +22 -7
|
@@ -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 commands/skills).
|
|
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 surface ECC's `aw:<stage>` skills via plugin
|
|
118
|
+
* marketplace + Skill permission.
|
|
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,21 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* c4/commandSurface.mjs — slash-command resolution per harness.
|
|
2
|
+
* c4/commandSurface.mjs — slash-command resolution per harness (G3).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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.
|
|
7
9
|
*
|
|
8
|
-
* Naming convention:
|
|
9
|
-
*
|
|
10
|
-
* ~/.
|
|
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.
|
|
11
16
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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.
|
|
15
22
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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.
|
|
19
28
|
*/
|
|
20
29
|
|
|
21
30
|
import {
|
|
@@ -27,48 +36,45 @@ import {
|
|
|
27
36
|
symlinkSync,
|
|
28
37
|
unlinkSync,
|
|
29
38
|
} from 'node:fs';
|
|
30
|
-
import { join } from 'node:path';
|
|
31
|
-
|
|
32
|
-
function awSkillsDir(awHome) {
|
|
33
|
-
return join(awHome, '.aw_registry', 'aw', 'skills');
|
|
34
|
-
}
|
|
39
|
+
import { join, basename, extname, relative } from 'node:path';
|
|
35
40
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
// Canonical AW routing-stage commands. Anything in <eccHome>/commands/*.md
|
|
42
|
+
// not on this list is treated as "not a slash-command" (e.g. README.md).
|
|
43
|
+
// This list is a filter applied AFTER globbing; it is not a source of truth
|
|
44
|
+
// for what gets installed — only what we recognize as a stage.
|
|
45
|
+
const AW_STAGE_COMMANDS = new Set([
|
|
46
|
+
'plan',
|
|
47
|
+
'build',
|
|
48
|
+
'investigate',
|
|
49
|
+
'review',
|
|
50
|
+
'test',
|
|
51
|
+
'deploy',
|
|
52
|
+
'ship',
|
|
53
|
+
'feature',
|
|
54
|
+
'adk',
|
|
55
|
+
'publish',
|
|
56
|
+
]);
|
|
41
57
|
|
|
42
58
|
/**
|
|
43
|
-
*
|
|
59
|
+
* Walk <eccHome>/commands/*.md and return the AW-stage basenames in
|
|
60
|
+
* alphabetical order. Stage filename matching is case-sensitive (ECC ships
|
|
61
|
+
* lowercase).
|
|
44
62
|
*
|
|
45
|
-
* @param {string}
|
|
46
|
-
* @returns {
|
|
63
|
+
* @param {string} eccHome
|
|
64
|
+
* @returns {string[]}
|
|
47
65
|
*/
|
|
48
|
-
function
|
|
49
|
-
const
|
|
50
|
-
if (!existsSync(
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
for (const entry of
|
|
54
|
-
if (entry.
|
|
55
|
-
const
|
|
56
|
-
if (
|
|
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
|
-
}
|
|
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);
|
|
66
75
|
}
|
|
67
|
-
|
|
68
|
-
return
|
|
69
|
-
const bySlash = a.slashName.localeCompare(b.slashName);
|
|
70
|
-
return bySlash === 0 ? a.skillName.localeCompare(b.skillName) : bySlash;
|
|
71
|
-
});
|
|
76
|
+
stageNames.sort();
|
|
77
|
+
return stageNames;
|
|
72
78
|
}
|
|
73
79
|
|
|
74
80
|
function harnessTargetDir(harness, home) {
|
|
@@ -93,40 +99,29 @@ function isCorrectSymlink(linkPath, expectedTarget) {
|
|
|
93
99
|
}
|
|
94
100
|
}
|
|
95
101
|
|
|
96
|
-
|
|
102
|
+
/**
|
|
103
|
+
* Symlink one stage command from ECC into the harness command directory,
|
|
104
|
+
* replacing a stale symlink if needed. Returns true on success.
|
|
105
|
+
*/
|
|
106
|
+
function linkStageCommand(stageName, eccHome, targetDir) {
|
|
107
|
+
const sourcePath = join(eccHome, 'commands', `${stageName}.md`);
|
|
108
|
+
const linkPath = join(targetDir, `${stageName}.md`);
|
|
109
|
+
if (isCorrectSymlink(linkPath, sourcePath)) return true;
|
|
110
|
+
// Replace any stale symlink or wrong-type entry.
|
|
97
111
|
try {
|
|
98
|
-
|
|
112
|
+
if (existsSync(linkPath) || lstatExists(linkPath)) {
|
|
113
|
+
try { unlinkSync(linkPath); } catch { /* may be a directory */ }
|
|
114
|
+
}
|
|
115
|
+
symlinkSync(sourcePath, linkPath);
|
|
99
116
|
return true;
|
|
100
117
|
} catch {
|
|
101
118
|
return false;
|
|
102
119
|
}
|
|
103
120
|
}
|
|
104
121
|
|
|
105
|
-
function
|
|
106
|
-
try {
|
|
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
|
-
|
|
122
|
+
function lstatExists(p) {
|
|
125
123
|
try {
|
|
126
|
-
|
|
127
|
-
try { unlinkSync(linkPath); } catch { /* may be a directory */ }
|
|
128
|
-
}
|
|
129
|
-
symlinkSync(route.sourcePath, linkPath);
|
|
124
|
+
lstatSync(p);
|
|
130
125
|
return true;
|
|
131
126
|
} catch {
|
|
132
127
|
return false;
|
|
@@ -134,16 +129,15 @@ function linkSkillRoute(route, targetDir) {
|
|
|
134
129
|
}
|
|
135
130
|
|
|
136
131
|
/**
|
|
137
|
-
* Ensure the harness slash surface for
|
|
132
|
+
* Ensure the harness slash-command surface for AW stages.
|
|
138
133
|
*
|
|
139
134
|
* @param {object} opts
|
|
140
135
|
* @param {'claude-web'|'cursor-cloud'|'codex-web'|string} opts.harness
|
|
141
136
|
* @param {string} opts.home
|
|
142
|
-
* @param {string} opts.
|
|
137
|
+
* @param {string} opts.eccHome
|
|
143
138
|
* @returns {{
|
|
144
139
|
* harness: string,
|
|
145
140
|
* expectedCommands: string[],
|
|
146
|
-
* expectedSkills: string[],
|
|
147
141
|
* found: string[],
|
|
148
142
|
* missing: string[],
|
|
149
143
|
* installedAction: 'symlink' | 'noop' | 'unsupported',
|
|
@@ -153,23 +147,23 @@ export function ensureCommandSurface(opts) {
|
|
|
153
147
|
if (!opts || typeof opts !== 'object') {
|
|
154
148
|
throw new Error('ensureCommandSurface: opts object is required');
|
|
155
149
|
}
|
|
156
|
-
const { harness, home,
|
|
150
|
+
const { harness, home, eccHome } = opts;
|
|
157
151
|
if (!home || typeof home !== 'string') {
|
|
158
152
|
throw new Error('ensureCommandSurface: opts.home is required');
|
|
159
153
|
}
|
|
160
|
-
if (!
|
|
161
|
-
throw new Error('ensureCommandSurface: opts.
|
|
154
|
+
if (!eccHome || typeof eccHome !== 'string') {
|
|
155
|
+
throw new Error('ensureCommandSurface: opts.eccHome is required');
|
|
162
156
|
}
|
|
163
157
|
|
|
164
|
-
const
|
|
165
|
-
const expectedCommands = routes.map((route) => route.slashName);
|
|
166
|
-
const expectedSkills = routes.map((route) => route.skillName);
|
|
158
|
+
const expectedCommands = discoverEccStageCommands(eccHome);
|
|
167
159
|
|
|
168
160
|
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.
|
|
169
164
|
return {
|
|
170
165
|
harness,
|
|
171
166
|
expectedCommands,
|
|
172
|
-
expectedSkills,
|
|
173
167
|
found: [...expectedCommands],
|
|
174
168
|
missing: [],
|
|
175
169
|
installedAction: 'noop',
|
|
@@ -181,31 +175,38 @@ export function ensureCommandSurface(opts) {
|
|
|
181
175
|
return {
|
|
182
176
|
harness,
|
|
183
177
|
expectedCommands,
|
|
184
|
-
expectedSkills,
|
|
185
178
|
found: [],
|
|
186
179
|
missing: [],
|
|
187
180
|
installedAction: 'unsupported',
|
|
188
181
|
};
|
|
189
182
|
}
|
|
190
183
|
|
|
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.
|
|
191
187
|
try {
|
|
192
188
|
mkdirSync(targetDir, { recursive: true });
|
|
193
189
|
} catch {
|
|
194
190
|
return {
|
|
195
191
|
harness,
|
|
196
192
|
expectedCommands,
|
|
197
|
-
expectedSkills,
|
|
198
193
|
found: [],
|
|
199
194
|
missing: [...expectedCommands],
|
|
200
195
|
installedAction: 'symlink',
|
|
201
196
|
};
|
|
202
197
|
}
|
|
203
|
-
|
|
204
|
-
|
|
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) {
|
|
205
207
|
return {
|
|
206
208
|
harness,
|
|
207
209
|
expectedCommands,
|
|
208
|
-
expectedSkills,
|
|
209
210
|
found: [],
|
|
210
211
|
missing: [...expectedCommands],
|
|
211
212
|
installedAction: 'symlink',
|
|
@@ -214,16 +215,15 @@ export function ensureCommandSurface(opts) {
|
|
|
214
215
|
|
|
215
216
|
const found = [];
|
|
216
217
|
const missing = [];
|
|
217
|
-
for (const
|
|
218
|
-
const ok =
|
|
219
|
-
if (ok) found.push(
|
|
220
|
-
else missing.push(
|
|
218
|
+
for (const stageName of expectedCommands) {
|
|
219
|
+
const ok = linkStageCommand(stageName, eccHome, targetDir);
|
|
220
|
+
if (ok) found.push(stageName);
|
|
221
|
+
else missing.push(stageName);
|
|
221
222
|
}
|
|
222
223
|
|
|
223
224
|
return {
|
|
224
225
|
harness,
|
|
225
226
|
expectedCommands,
|
|
226
|
-
expectedSkills,
|
|
227
227
|
found,
|
|
228
228
|
missing,
|
|
229
229
|
installedAction: 'symlink',
|
|
@@ -231,59 +231,161 @@ export function ensureCommandSurface(opts) {
|
|
|
231
231
|
}
|
|
232
232
|
|
|
233
233
|
/**
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
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.
|
|
237
341
|
*
|
|
238
342
|
* @param {object} opts
|
|
239
343
|
* @param {'claude-web'|'cursor-cloud'|'codex-web'|string} opts.harness
|
|
240
344
|
* @param {string} opts.home
|
|
241
|
-
* @param {string} [opts.awHome]
|
|
242
345
|
* @returns {{ expected: string[], found: string[], missing: string[], ok: boolean }}
|
|
243
346
|
*/
|
|
244
347
|
export function diagnoseCommandResolution(opts) {
|
|
245
348
|
if (!opts || typeof opts !== 'object') {
|
|
246
349
|
throw new Error('diagnoseCommandResolution: opts object is required');
|
|
247
350
|
}
|
|
248
|
-
const { harness, home
|
|
351
|
+
const { harness, home } = opts;
|
|
249
352
|
if (!home || typeof home !== 'string') {
|
|
250
353
|
throw new Error('diagnoseCommandResolution: opts.home is required');
|
|
251
354
|
}
|
|
252
|
-
|
|
253
|
-
const routes = awHome && typeof awHome === 'string' ? discoverAwSkillRoutes(awHome) : [];
|
|
254
|
-
const expected = routes.map((route) => route.slashName);
|
|
355
|
+
const expected = [...AW_STAGE_COMMANDS].sort();
|
|
255
356
|
|
|
256
357
|
if (harness === 'claude-web') {
|
|
257
|
-
|
|
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);
|
|
258
365
|
}
|
|
259
366
|
|
|
260
367
|
const targetDir = harnessTargetDir(harness, home);
|
|
261
368
|
if (!targetDir) {
|
|
262
369
|
return { expected, found: [], missing: [...expected], ok: false };
|
|
263
370
|
}
|
|
264
|
-
return
|
|
371
|
+
return walkCommandsDir(targetDir, expected);
|
|
265
372
|
}
|
|
266
373
|
|
|
267
|
-
function
|
|
374
|
+
function walkCommandsDir(dir, expected) {
|
|
268
375
|
if (!existsSync(dir)) {
|
|
269
|
-
const expected = routes.map((route) => route.slashName);
|
|
270
376
|
return { expected, found: [], missing: [...expected], ok: expected.length === 0 };
|
|
271
377
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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.
|
|
279
387
|
}
|
|
280
|
-
|
|
281
|
-
const
|
|
388
|
+
const found = expected.filter((n) => present.has(n));
|
|
389
|
+
const missing = expected.filter((n) => !present.has(n));
|
|
282
390
|
return { expected, found, missing, ok: missing.length === 0 };
|
|
283
391
|
}
|
|
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
|
-
*
|
|
7
|
+
* losing the contract from the matching command file even though the file
|
|
8
|
+
* 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
|
+
* command 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,35 +47,56 @@ 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 command file
|
|
51
51
|
alwaysApply: true
|
|
52
52
|
---
|
|
53
53
|
|
|
54
|
-
# AW
|
|
54
|
+
# AW slash-command expansion (Cursor Cloud workaround)
|
|
55
55
|
|
|
56
56
|
When the user message starts with \`/aw:<NAME>\` (case-sensitive; \`<NAME>\` may
|
|
57
|
-
contain hyphens
|
|
57
|
+
contain hyphens — both in the namespace path and in the command name), do
|
|
58
|
+
the following BEFORE producing any other response.
|
|
58
59
|
|
|
59
|
-
## 1.
|
|
60
|
+
## 1. Stage-command lookup (primary)
|
|
60
61
|
|
|
61
62
|
Read \`~/.cursor/commands/aw/<NAME>.md\`. If the file exists, jump to step 3
|
|
62
63
|
with its content as the resolved instructions.
|
|
63
64
|
|
|
64
|
-
## 2. Registry
|
|
65
|
+
## 2. Registry-command lookup (fallback, hyphen-disambiguation)
|
|
65
66
|
|
|
66
|
-
If step 1 did not resolve
|
|
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
|
|
67
75
|
repo registry root is \`.aw/.aw_registry/\` (relative to the agent's CWD —
|
|
68
76
|
the repo root in Cursor Cloud).
|
|
69
77
|
|
|
70
|
-
|
|
71
|
-
|
|
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\` ✓ |
|
|
72
93
|
|
|
73
94
|
## 3. Execute
|
|
74
95
|
|
|
75
96
|
Treat the resolved file's full content as your operating instructions for
|
|
76
|
-
this turn. Execute the
|
|
77
|
-
command in the user message. Do not summarize or paraphrase the
|
|
78
|
-
— execute it verbatim, including any "ask one question at a
|
|
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
|
|
79
100
|
time and wait" interaction protocol.
|
|
80
101
|
|
|
81
102
|
## 4. No match
|
|
@@ -83,10 +104,11 @@ time and wait" interaction protocol.
|
|
|
83
104
|
If neither lookup resolves to an existing file, do NOT silently fall back
|
|
84
105
|
to natural-language interpretation. Reply with exactly:
|
|
85
106
|
|
|
86
|
-
> \`/aw:<NAME>\` is not a registered AW
|
|
107
|
+
> \`/aw:<NAME>\` is not a registered AW command on this machine. Run
|
|
87
108
|
> \`aw c4 --diagnose\` and check the output of
|
|
88
|
-
> \`ls ~/.cursor/commands/aw/\` for available
|
|
89
|
-
> \`find .aw/.aw_registry
|
|
109
|
+
> \`ls ~/.cursor/commands/aw/\` for available stage commands and
|
|
110
|
+
> \`find .aw/.aw_registry -path '*/commands/*.md'\` for available registry
|
|
111
|
+
> commands.
|
|
90
112
|
|
|
91
113
|
This precise reply text is the smoke-test fingerprint for the rule itself.
|
|
92
114
|
The string is unique enough that it cannot be confused with the model's
|
|
@@ -95,8 +117,8 @@ natural-language fallback.
|
|
|
95
117
|
---
|
|
96
118
|
|
|
97
119
|
This rule exists because Cursor Cloud's chat UI does not pre-expand slash
|
|
98
|
-
commands the way Cursor Desktop does. The
|
|
99
|
-
|
|
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
|
|
100
122
|
\`.aw_docs/features/aw-c4-cursor-slash-shim/overview.md\` for context. Do
|
|
101
123
|
not edit this file by hand — \`aw c4 --harness cursor-cloud\` regenerates
|
|
102
124
|
it on every run.
|