@ghl-ai/aw 0.1.44-beta.0 → 0.1.44-beta.2
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 +142 -0
- package/c4/codexConfig.mjs +148 -0
- package/c4/codexPromptInjector.mjs +187 -0
- package/c4/commandSurface.mjs +286 -0
- package/c4/detect.mjs +62 -0
- package/c4/diagnostics.mjs +536 -0
- package/c4/eccRegistryBridge.mjs +184 -0
- package/c4/ghCli.mjs +94 -0
- package/c4/gitAuth.mjs +384 -0
- package/c4/index.mjs +64 -0
- package/c4/jsonMerge.mjs +229 -0
- package/c4/mcpServer.mjs +160 -0
- package/c4/mcpSmokeProbe.mjs +201 -0
- package/c4/preflight.mjs +254 -0
- package/c4/repoLocalClaudeSettings.mjs +54 -0
- package/c4/repoLocalIgnore.mjs +157 -0
- package/c4/repoRootInstructions.mjs +166 -0
- package/c4/secrets.mjs +55 -0
- package/c4/slimRouter.mjs +472 -0
- package/cli.mjs +7 -0
- package/commands/c4.mjs +387 -0
- package/ecc.mjs +7 -25
- package/integrate.mjs +6 -6
- package/mcp.mjs +2 -23
- package/package.json +5 -3
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* c4/commandSurface.mjs — slash-command resolution per harness (G3).
|
|
3
|
+
*
|
|
4
|
+
* Why: intent-based routing fires via the slim card / SessionStart hook,
|
|
5
|
+
* but a user typing `/aw:plan` directly relies on a per-harness slash
|
|
6
|
+
* command surface. Without these symlinks (or, on Claude, the plugin
|
|
7
|
+
* marketplace registration), `/aw:plan` errors out and the AC for
|
|
8
|
+
* "manual command works" fails.
|
|
9
|
+
*
|
|
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.
|
|
16
|
+
*
|
|
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.
|
|
22
|
+
*
|
|
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.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import {
|
|
31
|
+
existsSync,
|
|
32
|
+
lstatSync,
|
|
33
|
+
mkdirSync,
|
|
34
|
+
readdirSync,
|
|
35
|
+
readlinkSync,
|
|
36
|
+
symlinkSync,
|
|
37
|
+
unlinkSync,
|
|
38
|
+
} from 'node:fs';
|
|
39
|
+
import { join, basename, extname } 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
|
+
]);
|
|
57
|
+
|
|
58
|
+
/**
|
|
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).
|
|
62
|
+
*
|
|
63
|
+
* @param {string} eccHome
|
|
64
|
+
* @returns {string[]}
|
|
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);
|
|
75
|
+
}
|
|
76
|
+
stageNames.sort();
|
|
77
|
+
return stageNames;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function harnessTargetDir(harness, home) {
|
|
81
|
+
if (harness === 'cursor-cloud') return join(home, '.cursor/commands/aw');
|
|
82
|
+
if (harness === 'codex-web') return join(home, '.codex/commands/aw');
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function isCorrectSymlink(linkPath, expectedTarget) {
|
|
87
|
+
if (!existsSync(linkPath)) return false;
|
|
88
|
+
let stat;
|
|
89
|
+
try {
|
|
90
|
+
stat = lstatSync(linkPath);
|
|
91
|
+
} catch {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
if (!stat.isSymbolicLink()) return false;
|
|
95
|
+
try {
|
|
96
|
+
return readlinkSync(linkPath) === expectedTarget;
|
|
97
|
+
} catch {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
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.
|
|
111
|
+
try {
|
|
112
|
+
if (existsSync(linkPath) || lstatExists(linkPath)) {
|
|
113
|
+
try { unlinkSync(linkPath); } catch { /* may be a directory */ }
|
|
114
|
+
}
|
|
115
|
+
symlinkSync(sourcePath, linkPath);
|
|
116
|
+
return true;
|
|
117
|
+
} catch {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function lstatExists(p) {
|
|
123
|
+
try {
|
|
124
|
+
lstatSync(p);
|
|
125
|
+
return true;
|
|
126
|
+
} catch {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Ensure the harness slash-command surface for AW stages.
|
|
133
|
+
*
|
|
134
|
+
* @param {object} opts
|
|
135
|
+
* @param {'claude-web'|'cursor-cloud'|'codex-web'|string} opts.harness
|
|
136
|
+
* @param {string} opts.home
|
|
137
|
+
* @param {string} opts.eccHome
|
|
138
|
+
* @returns {{
|
|
139
|
+
* harness: string,
|
|
140
|
+
* expectedCommands: string[],
|
|
141
|
+
* found: string[],
|
|
142
|
+
* missing: string[],
|
|
143
|
+
* installedAction: 'symlink' | 'noop' | 'unsupported',
|
|
144
|
+
* }}
|
|
145
|
+
*/
|
|
146
|
+
export function ensureCommandSurface(opts) {
|
|
147
|
+
if (!opts || typeof opts !== 'object') {
|
|
148
|
+
throw new Error('ensureCommandSurface: opts object is required');
|
|
149
|
+
}
|
|
150
|
+
const { harness, home, eccHome } = opts;
|
|
151
|
+
if (!home || typeof home !== 'string') {
|
|
152
|
+
throw new Error('ensureCommandSurface: opts.home is required');
|
|
153
|
+
}
|
|
154
|
+
if (!eccHome || typeof eccHome !== 'string') {
|
|
155
|
+
throw new Error('ensureCommandSurface: opts.eccHome is required');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const expectedCommands = discoverEccStageCommands(eccHome);
|
|
159
|
+
|
|
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.
|
|
164
|
+
return {
|
|
165
|
+
harness,
|
|
166
|
+
expectedCommands,
|
|
167
|
+
found: [...expectedCommands],
|
|
168
|
+
missing: [],
|
|
169
|
+
installedAction: 'noop',
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const targetDir = harnessTargetDir(harness, home);
|
|
174
|
+
if (!targetDir) {
|
|
175
|
+
return {
|
|
176
|
+
harness,
|
|
177
|
+
expectedCommands,
|
|
178
|
+
found: [],
|
|
179
|
+
missing: [],
|
|
180
|
+
installedAction: 'unsupported',
|
|
181
|
+
};
|
|
182
|
+
}
|
|
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.
|
|
187
|
+
try {
|
|
188
|
+
mkdirSync(targetDir, { recursive: true });
|
|
189
|
+
} catch {
|
|
190
|
+
return {
|
|
191
|
+
harness,
|
|
192
|
+
expectedCommands,
|
|
193
|
+
found: [],
|
|
194
|
+
missing: [...expectedCommands],
|
|
195
|
+
installedAction: 'symlink',
|
|
196
|
+
};
|
|
197
|
+
}
|
|
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) {
|
|
207
|
+
return {
|
|
208
|
+
harness,
|
|
209
|
+
expectedCommands,
|
|
210
|
+
found: [],
|
|
211
|
+
missing: [...expectedCommands],
|
|
212
|
+
installedAction: 'symlink',
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const found = [];
|
|
217
|
+
const missing = [];
|
|
218
|
+
for (const stageName of expectedCommands) {
|
|
219
|
+
const ok = linkStageCommand(stageName, eccHome, targetDir);
|
|
220
|
+
if (ok) found.push(stageName);
|
|
221
|
+
else missing.push(stageName);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
harness,
|
|
226
|
+
expectedCommands,
|
|
227
|
+
found,
|
|
228
|
+
missing,
|
|
229
|
+
installedAction: 'symlink',
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Read-only diagnostic. Walks the harness command directory and reports
|
|
235
|
+
* which AW stage commands are resolvable. Does not fix.
|
|
236
|
+
*
|
|
237
|
+
* @param {object} opts
|
|
238
|
+
* @param {'claude-web'|'cursor-cloud'|'codex-web'|string} opts.harness
|
|
239
|
+
* @param {string} opts.home
|
|
240
|
+
* @returns {{ expected: string[], found: string[], missing: string[], ok: boolean }}
|
|
241
|
+
*/
|
|
242
|
+
export function diagnoseCommandResolution(opts) {
|
|
243
|
+
if (!opts || typeof opts !== 'object') {
|
|
244
|
+
throw new Error('diagnoseCommandResolution: opts object is required');
|
|
245
|
+
}
|
|
246
|
+
const { harness, home } = opts;
|
|
247
|
+
if (!home || typeof home !== 'string') {
|
|
248
|
+
throw new Error('diagnoseCommandResolution: opts.home is required');
|
|
249
|
+
}
|
|
250
|
+
const expected = [...AW_STAGE_COMMANDS].sort();
|
|
251
|
+
|
|
252
|
+
if (harness === 'claude-web') {
|
|
253
|
+
// Plugin marketplace path. If a `~/.claude/commands` dir exists we
|
|
254
|
+
// honor it; otherwise we trust marketplace dispatch and report ok.
|
|
255
|
+
const claudeDir = join(home, '.claude/commands');
|
|
256
|
+
if (!existsSync(claudeDir)) {
|
|
257
|
+
return { expected, found: [], missing: [], ok: true };
|
|
258
|
+
}
|
|
259
|
+
return walkCommandsDir(claudeDir, expected);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const targetDir = harnessTargetDir(harness, home);
|
|
263
|
+
if (!targetDir) {
|
|
264
|
+
return { expected, found: [], missing: [...expected], ok: false };
|
|
265
|
+
}
|
|
266
|
+
return walkCommandsDir(targetDir, expected);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function walkCommandsDir(dir, expected) {
|
|
270
|
+
if (!existsSync(dir)) {
|
|
271
|
+
return { expected, found: [], missing: [...expected], ok: expected.length === 0 };
|
|
272
|
+
}
|
|
273
|
+
const present = new Set();
|
|
274
|
+
try {
|
|
275
|
+
for (const entry of readdirSync(dir)) {
|
|
276
|
+
if (extname(entry).toLowerCase() !== '.md') continue;
|
|
277
|
+
const name = basename(entry, '.md');
|
|
278
|
+
if (AW_STAGE_COMMANDS.has(name)) present.add(name);
|
|
279
|
+
}
|
|
280
|
+
} catch {
|
|
281
|
+
// Unreadable directory: treat as nothing-found.
|
|
282
|
+
}
|
|
283
|
+
const found = expected.filter((n) => present.has(n));
|
|
284
|
+
const missing = expected.filter((n) => !present.has(n));
|
|
285
|
+
return { expected, found, missing, ok: missing.length === 0 };
|
|
286
|
+
}
|
package/c4/detect.mjs
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* c4/detect.mjs — identify the cloud harness this VM is running inside.
|
|
3
|
+
*
|
|
4
|
+
* Returned values (canonical):
|
|
5
|
+
* 'cursor-cloud' | 'codex-web' | 'claude-web' | 'unknown'
|
|
6
|
+
*
|
|
7
|
+
* Detection signals (precedence top → bottom):
|
|
8
|
+
* 1. cursor-cloud ← env.CURSOR_AGENT === 'true' || env.CURSOR_BACKGROUND_AGENT_ID
|
|
9
|
+
* 2. codex-web ← env.CODEX_ENVIRONMENT === '1' || /\.codex(\/|$)/.test(cwd)
|
|
10
|
+
* 3. claude-web ← env.CLAUDE_CODE_REMOTE === 'true' || fsProbe('/home/user/.claude')
|
|
11
|
+
* 4. unknown ← fallback
|
|
12
|
+
*
|
|
13
|
+
* Override: env.AW_C4_HARNESS (a known harness id) wins over all signals.
|
|
14
|
+
*
|
|
15
|
+
* @typedef {'cursor-cloud' | 'codex-web' | 'claude-web' | 'unknown'} HarnessId
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { existsSync } from 'node:fs';
|
|
19
|
+
|
|
20
|
+
export const HARNESSES = Object.freeze(['cursor-cloud', 'codex-web', 'claude-web', 'unknown']);
|
|
21
|
+
|
|
22
|
+
const CODEX_CWD_PATTERN = /\.codex(\/|$)/;
|
|
23
|
+
|
|
24
|
+
function defaultFsProbe(path) {
|
|
25
|
+
try {
|
|
26
|
+
return existsSync(path);
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {object} [opts]
|
|
34
|
+
* @param {NodeJS.ProcessEnv} [opts.env] Environment to inspect (defaults to process.env)
|
|
35
|
+
* @param {(path: string) => boolean} [opts.fsProbe] Filesystem existence probe (defaults to fs.existsSync)
|
|
36
|
+
* @param {string} [opts.cwd] Current working directory (defaults to process.cwd())
|
|
37
|
+
* @returns {HarnessId}
|
|
38
|
+
*/
|
|
39
|
+
export function detectHarness({
|
|
40
|
+
env = process.env,
|
|
41
|
+
fsProbe = defaultFsProbe,
|
|
42
|
+
cwd = process.cwd(),
|
|
43
|
+
} = {}) {
|
|
44
|
+
const override = env.AW_C4_HARNESS;
|
|
45
|
+
if (override && HARNESSES.includes(override)) {
|
|
46
|
+
return /** @type {HarnessId} */ (override);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (env.CURSOR_AGENT === 'true' || env.CURSOR_BACKGROUND_AGENT_ID) {
|
|
50
|
+
return 'cursor-cloud';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (env.CODEX_ENVIRONMENT === '1' || CODEX_CWD_PATTERN.test(cwd)) {
|
|
54
|
+
return 'codex-web';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (env.CLAUDE_CODE_REMOTE === 'true' || fsProbe('/home/user/.claude')) {
|
|
58
|
+
return 'claude-web';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return 'unknown';
|
|
62
|
+
}
|