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