@ghl-ai/aw 0.1.70-beta.2 → 0.1.70-beta.4
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 +45 -23
- 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 +74 -3
- package/link.mjs +28 -47
- package/package.json +1 -1
- package/startup.mjs +22 -7
package/c4/diagnostics.mjs
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* diagnoseAwRouterView({ awHome })
|
|
11
11
|
* Codex-relevant: replays the upstream `find * /skills/* /SKILL.md`
|
|
12
|
-
* enumeration logic against ~/.aw/.aw_registry/
|
|
12
|
+
* enumeration logic against ~/.aw/.aw_registry/platform/core/skills/
|
|
13
13
|
* and reports which expected stage skills are visible to the
|
|
14
14
|
* SessionStart hook. Catches Bug A failures (registry path empty
|
|
15
15
|
* post-init).
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* Tests both that the wrapper is registered AND that it actually
|
|
23
23
|
* emits the router (catches "wrapper installed but does nothing").
|
|
24
24
|
*
|
|
25
|
-
* diagnoseSkillResolution({ harness, home, awHome, skills })
|
|
25
|
+
* diagnoseSkillResolution({ harness, home, awHome, eccHome, skills })
|
|
26
26
|
* Multi-path × multi-skill probe. For each skill, checks four
|
|
27
27
|
* candidate paths in priority order. Top-level ok=true ONLY if
|
|
28
28
|
* using-aw-skills AND every stage skill the slim card promises
|
|
@@ -146,8 +146,8 @@ function slashShimSummaryToken(action) {
|
|
|
146
146
|
* ───────────────────────────────────────────────────────────────────────── */
|
|
147
147
|
|
|
148
148
|
/**
|
|
149
|
-
* Replay of the
|
|
150
|
-
* find <awHome>/.aw_registry/
|
|
149
|
+
* Replay of the upstream session-start.sh enumerator scan:
|
|
150
|
+
* find <awHome>/.aw_registry/platform -path STAR/skills/STAR/SKILL.md
|
|
151
151
|
*
|
|
152
152
|
* @param {object} opts
|
|
153
153
|
* @param {string} opts.awHome
|
|
@@ -156,8 +156,8 @@ function slashShimSummaryToken(action) {
|
|
|
156
156
|
export function diagnoseAwRouterView({ awHome } = {}) {
|
|
157
157
|
if (!awHome) throw new Error('diagnoseAwRouterView: awHome is required');
|
|
158
158
|
|
|
159
|
-
const
|
|
160
|
-
const enumerableSkills =
|
|
159
|
+
const platformRoot = join(awHome, '.aw_registry/platform');
|
|
160
|
+
const enumerableSkills = enumerateRegistrySkills(platformRoot);
|
|
161
161
|
const expected = [...EXPECTED_STAGE_SKILLS];
|
|
162
162
|
const missing = expected.filter((s) => !enumerableSkills.includes(s));
|
|
163
163
|
const ok = missing.length === 0;
|
|
@@ -166,23 +166,38 @@ export function diagnoseAwRouterView({ awHome } = {}) {
|
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
/**
|
|
169
|
-
* Walk <root>/<skill>/SKILL.md
|
|
169
|
+
* Walk <root>/<team>/skills/<skill>/SKILL.md two levels deep, mirroring the
|
|
170
|
+
* upstream find pattern. Returns the deduped sorted list of skill
|
|
171
|
+
* directory names.
|
|
170
172
|
*/
|
|
171
|
-
function
|
|
172
|
-
if (!existsSync(
|
|
173
|
+
function enumerateRegistrySkills(platformRoot) {
|
|
174
|
+
if (!existsSync(platformRoot)) return [];
|
|
173
175
|
const set = new Set();
|
|
174
|
-
let
|
|
176
|
+
let teams;
|
|
175
177
|
try {
|
|
176
|
-
|
|
178
|
+
teams = readdirSync(platformRoot, { withFileTypes: true });
|
|
177
179
|
} catch {
|
|
178
|
-
// readdir on the
|
|
180
|
+
// readdir on the platform root failed (EACCES, ENOENT). Treat as
|
|
179
181
|
// "no skills enumerable" — the caller's missing-list will surface it.
|
|
180
182
|
return [];
|
|
181
183
|
}
|
|
182
|
-
for (const
|
|
183
|
-
if (!
|
|
184
|
-
const
|
|
185
|
-
if (existsSync(
|
|
184
|
+
for (const team of teams) {
|
|
185
|
+
if (!team.isDirectory()) continue;
|
|
186
|
+
const skillsDir = join(platformRoot, team.name, 'skills');
|
|
187
|
+
if (!existsSync(skillsDir)) continue;
|
|
188
|
+
let skills;
|
|
189
|
+
try {
|
|
190
|
+
skills = readdirSync(skillsDir, { withFileTypes: true });
|
|
191
|
+
} catch {
|
|
192
|
+
// readdir on a team's skills/ subdirectory failed — skip this team
|
|
193
|
+
// dir but continue probing siblings.
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
for (const skill of skills) {
|
|
197
|
+
if (!skill.isDirectory()) continue;
|
|
198
|
+
const skillMd = join(skillsDir, skill.name, 'SKILL.md');
|
|
199
|
+
if (existsSync(skillMd)) set.add(skill.name);
|
|
200
|
+
}
|
|
186
201
|
}
|
|
187
202
|
return [...set].sort();
|
|
188
203
|
}
|
|
@@ -322,6 +337,7 @@ function envelopeContainsRouter(output) {
|
|
|
322
337
|
* @param {Harness} opts.harness
|
|
323
338
|
* @param {string} opts.home
|
|
324
339
|
* @param {string} opts.awHome
|
|
340
|
+
* @param {string} opts.eccHome
|
|
325
341
|
* @param {string[]} [opts.skills] Default: DEFAULT_SKILL_LIST
|
|
326
342
|
* @returns {{
|
|
327
343
|
* perSkill: SkillResolutionResult[],
|
|
@@ -334,11 +350,12 @@ export function diagnoseSkillResolution(opts = {}) {
|
|
|
334
350
|
if (!opts.harness) throw new Error('diagnoseSkillResolution: harness is required');
|
|
335
351
|
if (!opts.home) throw new Error('diagnoseSkillResolution: home is required');
|
|
336
352
|
if (!opts.awHome) throw new Error('diagnoseSkillResolution: awHome is required');
|
|
353
|
+
if (!opts.eccHome) throw new Error('diagnoseSkillResolution: eccHome is required');
|
|
337
354
|
|
|
338
|
-
const { harness, home, awHome } = opts;
|
|
355
|
+
const { harness, home, awHome, eccHome } = opts;
|
|
339
356
|
const skills = opts.skills ?? [...DEFAULT_SKILL_LIST];
|
|
340
357
|
|
|
341
|
-
const perSkill = skills.map((skill) => probeOneSkill(skill, harness, home, awHome));
|
|
358
|
+
const perSkill = skills.map((skill) => probeOneSkill(skill, harness, home, awHome, eccHome));
|
|
342
359
|
const routerSkill = perSkill.find((e) => e.skill === ROUTER_SKILL) ?? {
|
|
343
360
|
skill: ROUTER_SKILL,
|
|
344
361
|
candidatePathsChecked: [],
|
|
@@ -357,11 +374,11 @@ function harnessSkillsDir(harness, home) {
|
|
|
357
374
|
if (harness === 'claude-web') return join(home, '.claude/skills');
|
|
358
375
|
if (harness === 'cursor-cloud') return join(home, '.cursor/skills');
|
|
359
376
|
// codex-web: registry path is the canonical source; the per-harness skills
|
|
360
|
-
// dir is conventionally absent. We still probe the
|
|
377
|
+
// dir is conventionally absent. We still probe the registry + ECC paths.
|
|
361
378
|
return null;
|
|
362
379
|
}
|
|
363
380
|
|
|
364
|
-
function probeOneSkill(skill, harness, home, awHome) {
|
|
381
|
+
function probeOneSkill(skill, harness, home, awHome, eccHome) {
|
|
365
382
|
const harnessDir = harnessSkillsDir(harness, home);
|
|
366
383
|
const candidatePathsChecked = [];
|
|
367
384
|
|
|
@@ -369,7 +386,8 @@ function probeOneSkill(skill, harness, home, awHome) {
|
|
|
369
386
|
candidatePathsChecked.push(join(harnessDir, skill, 'SKILL.md'));
|
|
370
387
|
candidatePathsChecked.push(join(harnessDir, `platform-core-${skill}`, 'SKILL.md'));
|
|
371
388
|
}
|
|
372
|
-
candidatePathsChecked.push(join(awHome, '.aw_registry/
|
|
389
|
+
candidatePathsChecked.push(join(awHome, '.aw_registry/platform/core/skills', skill, 'SKILL.md'));
|
|
390
|
+
candidatePathsChecked.push(join(eccHome, 'skills', skill, 'SKILL.md'));
|
|
373
391
|
|
|
374
392
|
let found = null;
|
|
375
393
|
for (const p of candidatePathsChecked) {
|
|
@@ -526,7 +544,7 @@ function enumerateConfigKeysOnly(filePath) {
|
|
|
526
544
|
}
|
|
527
545
|
|
|
528
546
|
function sampleRegistrySkillPaths(awHome, max) {
|
|
529
|
-
const root = join(awHome, '.aw_registry/
|
|
547
|
+
const root = join(awHome, '.aw_registry/platform');
|
|
530
548
|
if (!existsSync(root)) return [' <registry not provisioned>'];
|
|
531
549
|
const out = [];
|
|
532
550
|
walkSkillMd(root, out, max);
|
|
@@ -551,7 +569,8 @@ function walkSkillMd(dir, out, max) {
|
|
|
551
569
|
} else if (e.isDirectory()) {
|
|
552
570
|
walkSkillMd(p, out, max);
|
|
553
571
|
} else if (e.isSymbolicLink()) {
|
|
554
|
-
// Cheap stat check — symlinks pointing at SKILL.md
|
|
572
|
+
// Cheap stat check — symlinks pointing at SKILL.md (the bridge case)
|
|
573
|
+
// should still appear in the dump.
|
|
555
574
|
try {
|
|
556
575
|
const st = statSync(p);
|
|
557
576
|
if (st.isFile() && e.name === 'SKILL.md') out.push(p);
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* c4/eccRegistryBridge.mjs — Codex-only Bug A workaround.
|
|
3
|
+
*
|
|
4
|
+
* Codex Web's upstream `aw-session-start.sh` enumerator scans a fixed
|
|
5
|
+
* registry path:
|
|
6
|
+
*
|
|
7
|
+
* ~/.aw/.aw_registry/platform/core/skills/<name>/SKILL.md
|
|
8
|
+
*
|
|
9
|
+
* ECC ships skills under a different path:
|
|
10
|
+
*
|
|
11
|
+
* ~/.aw-ecc/skills/<name>/SKILL.md
|
|
12
|
+
*
|
|
13
|
+
* On Codex Web, neither Claude's plugin marketplace nor Cursor's direct
|
|
14
|
+
* `~/.cursor/skills/...` resolution applies. The only way Codex can see
|
|
15
|
+
* ECC skills is if the registry path is populated. We bridge by symlinking
|
|
16
|
+
* each ECC skill's SKILL.md into the registry layout. The default mode keeps
|
|
17
|
+
* the original non-clobbering behavior. `preferEccSkills` only retargets
|
|
18
|
+
* existing symlinks; regular files are treated as user-owned and preserved.
|
|
19
|
+
*
|
|
20
|
+
* Also installs a fallback symlink at `<homeOf(awHome)>/.aw_registry` →
|
|
21
|
+
* `<awHome>/.aw_registry` because some legacy hooks read from that path.
|
|
22
|
+
*
|
|
23
|
+
* **Scope**: Codex Web only. The orchestrator is responsible for *not*
|
|
24
|
+
* invoking this on claude-web or cursor-cloud; the module itself is
|
|
25
|
+
* harness-agnostic and just performs the bridge.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import {
|
|
29
|
+
existsSync,
|
|
30
|
+
lstatSync,
|
|
31
|
+
readlinkSync,
|
|
32
|
+
readdirSync,
|
|
33
|
+
mkdirSync,
|
|
34
|
+
symlinkSync,
|
|
35
|
+
unlinkSync,
|
|
36
|
+
} from 'node:fs';
|
|
37
|
+
import { dirname, join } from 'node:path';
|
|
38
|
+
|
|
39
|
+
/** True iff `path` is a symlink that points to a nonexistent target. */
|
|
40
|
+
function isBrokenSymlink(path) {
|
|
41
|
+
try {
|
|
42
|
+
const st = lstatSync(path);
|
|
43
|
+
if (!st.isSymbolicLink()) return false;
|
|
44
|
+
return !existsSync(path);
|
|
45
|
+
} catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** True iff `path` is a symlink that already points at `target`. */
|
|
51
|
+
function isSymlinkTo(path, target) {
|
|
52
|
+
try {
|
|
53
|
+
const st = lstatSync(path);
|
|
54
|
+
if (!st.isSymbolicLink()) return false;
|
|
55
|
+
return readlinkSync(path) === target;
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** True iff `path` is any symlink, regardless of where it points. */
|
|
62
|
+
function isSymlink(path) {
|
|
63
|
+
try {
|
|
64
|
+
return lstatSync(path).isSymbolicLink();
|
|
65
|
+
} catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Install the fallback symlink `~/.aw_registry → ~/.aw/.aw_registry` if not
|
|
72
|
+
* present and not blocked by an existing real directory. Best-effort.
|
|
73
|
+
*/
|
|
74
|
+
function installRegistryFallbackLink(awHome) {
|
|
75
|
+
const home = dirname(awHome);
|
|
76
|
+
const fallback = join(home, '.aw_registry');
|
|
77
|
+
const target = join(awHome, '.aw_registry');
|
|
78
|
+
|
|
79
|
+
if (!existsSync(target)) {
|
|
80
|
+
// Nothing to point at yet — skip silently.
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let existsAtFallback = false;
|
|
85
|
+
try {
|
|
86
|
+
lstatSync(fallback);
|
|
87
|
+
existsAtFallback = true;
|
|
88
|
+
} catch {
|
|
89
|
+
existsAtFallback = false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (existsAtFallback) return; // do not clobber existing dir/symlink
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
symlinkSync(target, fallback);
|
|
96
|
+
} catch {
|
|
97
|
+
// best-effort
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Symlink every ECC skill into the registry layout. Idempotent.
|
|
103
|
+
*
|
|
104
|
+
* Result shape:
|
|
105
|
+
* - linked — skill names freshly bridged this call
|
|
106
|
+
* - skipped — skill names whose registry target already existed and was preserved
|
|
107
|
+
* - replaced — skill names whose registry target was replaced with the ECC symlink
|
|
108
|
+
* - broken — skill directories that exist in eccHome/skills/ but have no SKILL.md
|
|
109
|
+
* (broken ECC layout — distinct from already-bridged so the orchestrator
|
|
110
|
+
* can surface a real diagnostic rather than treating it as success)
|
|
111
|
+
* - reason — only set when the result is fully diagnostic:
|
|
112
|
+
* 'ecc-missing' — eccHome/skills/ does not exist
|
|
113
|
+
* 'already-bridged' — every well-formed skill was already linked
|
|
114
|
+
* and there were no broken skills
|
|
115
|
+
*
|
|
116
|
+
* @param {object} opts
|
|
117
|
+
* @param {string} opts.awHome Path of the AW home (e.g. ~/.aw).
|
|
118
|
+
* @param {string} opts.eccHome Path of the ECC home (e.g. ~/.aw-ecc).
|
|
119
|
+
* @param {boolean} [opts.preferEccSkills=false]
|
|
120
|
+
* Retarget existing registry SKILL.md symlinks to ECC. Regular files are
|
|
121
|
+
* never replaced because they may be user-owned overrides.
|
|
122
|
+
* @returns {{
|
|
123
|
+
* linked: string[],
|
|
124
|
+
* skipped: string[],
|
|
125
|
+
* replaced: string[],
|
|
126
|
+
* broken: string[],
|
|
127
|
+
* reason?: 'already-bridged' | 'ecc-missing'
|
|
128
|
+
* }}
|
|
129
|
+
*/
|
|
130
|
+
export function applyEccRegistryBridge({ awHome, eccHome, preferEccSkills = false } = {}) {
|
|
131
|
+
if (!awHome || !eccHome) {
|
|
132
|
+
throw new Error('applyEccRegistryBridge: awHome and eccHome are required');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const eccSkillsDir = join(eccHome, 'skills');
|
|
136
|
+
if (!existsSync(eccSkillsDir)) {
|
|
137
|
+
return { linked: [], skipped: [], broken: [], reason: 'ecc-missing' };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const linked = [];
|
|
141
|
+
const skipped = [];
|
|
142
|
+
const replaced = [];
|
|
143
|
+
const broken = [];
|
|
144
|
+
|
|
145
|
+
let entries;
|
|
146
|
+
try {
|
|
147
|
+
entries = readdirSync(eccSkillsDir, { withFileTypes: true });
|
|
148
|
+
} catch {
|
|
149
|
+
return { linked: [], skipped: [], broken: [], reason: 'ecc-missing' };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let skillDirCount = 0;
|
|
153
|
+
for (const entry of entries) {
|
|
154
|
+
if (!entry.isDirectory()) continue;
|
|
155
|
+
skillDirCount += 1;
|
|
156
|
+
const name = entry.name;
|
|
157
|
+
const source = join(eccSkillsDir, name, 'SKILL.md');
|
|
158
|
+
|
|
159
|
+
// ECC skill directory exists but lacks SKILL.md → broken layout.
|
|
160
|
+
if (!existsSync(source)) {
|
|
161
|
+
broken.push(name);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const targetDir = join(awHome, '.aw_registry/platform/core/skills', name);
|
|
166
|
+
const target = join(targetDir, 'SKILL.md');
|
|
167
|
+
|
|
168
|
+
// If target already exists and is healthy, preserve regular files. The
|
|
169
|
+
// optional preference mode may only retarget generated symlinks; it must
|
|
170
|
+
// not delete user-owned registry files.
|
|
171
|
+
let targetExists = false;
|
|
172
|
+
try {
|
|
173
|
+
lstatSync(target);
|
|
174
|
+
targetExists = true;
|
|
175
|
+
} catch {
|
|
176
|
+
targetExists = false;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (targetExists && !isBrokenSymlink(target)) {
|
|
180
|
+
if (isSymlinkTo(target, source)) {
|
|
181
|
+
skipped.push(name);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (!preferEccSkills || !isSymlink(target)) {
|
|
186
|
+
skipped.push(name);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
unlinkSync(target);
|
|
192
|
+
} catch {
|
|
193
|
+
broken.push(name);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
symlinkSync(source, target);
|
|
199
|
+
replaced.push(name);
|
|
200
|
+
} catch {
|
|
201
|
+
broken.push(name);
|
|
202
|
+
}
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Either absent or a broken symlink — replace.
|
|
207
|
+
mkdirSync(targetDir, { recursive: true });
|
|
208
|
+
if (targetExists) {
|
|
209
|
+
try { unlinkSync(target); } catch { /* best-effort */ }
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
symlinkSync(source, target);
|
|
213
|
+
linked.push(name);
|
|
214
|
+
} catch {
|
|
215
|
+
// Filesystem may forbid symlinks (rare on Codex Web). Treat as broken
|
|
216
|
+
// so the orchestrator can surface the real diagnostic.
|
|
217
|
+
broken.push(name);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
installRegistryFallbackLink(awHome);
|
|
222
|
+
|
|
223
|
+
const result = { linked, skipped, replaced, broken };
|
|
224
|
+
// Only flag 'already-bridged' when every well-formed skill was already linked
|
|
225
|
+
// AND no broken skills exist — i.e. this run was a true no-op.
|
|
226
|
+
if (
|
|
227
|
+
skillDirCount > 0 &&
|
|
228
|
+
linked.length === 0 &&
|
|
229
|
+
replaced.length === 0 &&
|
|
230
|
+
broken.length === 0 &&
|
|
231
|
+
skipped.length === skillDirCount
|
|
232
|
+
) {
|
|
233
|
+
result.reason = 'already-bridged';
|
|
234
|
+
}
|
|
235
|
+
return result;
|
|
236
|
+
}
|
package/c4/index.mjs
CHANGED
|
@@ -29,6 +29,7 @@ export {
|
|
|
29
29
|
verifyAuth,
|
|
30
30
|
preflightPlatformDocs,
|
|
31
31
|
} from './gitAuth.mjs';
|
|
32
|
+
export { applyEccRegistryBridge } from './eccRegistryBridge.mjs';
|
|
32
33
|
export {
|
|
33
34
|
SLIM_CARD_HARD_CEILING_BYTES,
|
|
34
35
|
REQUIRED_ENFORCEMENT_PHRASES,
|
|
@@ -48,7 +49,7 @@ export {
|
|
|
48
49
|
export { MCP_URL_DEFAULT, registerGhlAiMcp } from './mcpServer.mjs';
|
|
49
50
|
export { probeMcpServer } from './mcpSmokeProbe.mjs';
|
|
50
51
|
export { ensureClaudeMarketplace } from './claudePluginRegistry.mjs';
|
|
51
|
-
export { ensureCommandSurface, diagnoseCommandResolution } from './commandSurface.mjs';
|
|
52
|
+
export { ensureCommandSurface, ensureRegistryCommandSurface, diagnoseCommandResolution } from './commandSurface.mjs';
|
|
52
53
|
export { installCursorSlashShim, CURSOR_SLASH_SHIM_RULE } from './cursorRulesShim.mjs';
|
|
53
54
|
export { ensureRepoLocalClaudeSettings } from './repoLocalClaudeSettings.mjs';
|
|
54
55
|
export { copyRepoRootInstructions } from './repoRootInstructions.mjs';
|
package/c4/preflight.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* c4/preflight.mjs — preflight aggregator for `aw c4`.
|
|
3
3
|
*
|
|
4
4
|
* Composes harness, token resolution, REST-API auth verification, gh CLI
|
|
5
|
-
* presence,
|
|
5
|
+
* presence, ECC bridge-status probe, and disk-free reporting into a single
|
|
6
6
|
* PreflightResult shape with per-failure recommendation strings. Pure
|
|
7
7
|
* orchestration over already-tested c4 primitives — no new I/O patterns.
|
|
8
8
|
*
|
|
@@ -71,10 +71,10 @@ const REC = Object.freeze({
|
|
|
71
71
|
`Insufficient disk: ${mb}MB free (aw c4 needs at least ${DISK_FREE_THRESHOLD_MB}MB ` +
|
|
72
72
|
'for ECC clone + registry pull).',
|
|
73
73
|
bridgeWillInstall:
|
|
74
|
-
'
|
|
75
|
-
'
|
|
74
|
+
'ECC registry bridge will be installed during this run (Codex only — ' +
|
|
75
|
+
'Bug A workaround that symlinks ECC stage skills into ~/.aw/.aw_registry).',
|
|
76
76
|
bridgeFailed:
|
|
77
|
-
'AW registry
|
|
77
|
+
'ECC and/or AW registry not yet provisioned — aw init will create them ' +
|
|
78
78
|
'on the first c4 run; re-run preflight afterwards.',
|
|
79
79
|
});
|
|
80
80
|
|
|
@@ -120,18 +120,22 @@ function classifyAuthError(verifyResult) {
|
|
|
120
120
|
return 'unknown';
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
function probeBridgeStatus(harness, awHome) {
|
|
123
|
+
function probeBridgeStatus(harness, awHome, eccHome) {
|
|
124
124
|
if (harness !== 'codex-web') return 'ok';
|
|
125
125
|
|
|
126
|
-
//
|
|
127
|
-
// If aw-plan is
|
|
128
|
-
|
|
126
|
+
// Bridge installs ~/.aw/.aw_registry/platform/core/skills/<aw-*>/SKILL.md
|
|
127
|
+
// symlinks pointing at ~/.aw-ecc/skills/<aw-*>/SKILL.md. If aw-plan is
|
|
128
|
+
// already present in the registry path, the bridge is a no-op (ok).
|
|
129
|
+
const bridgedSkillPath = join(
|
|
129
130
|
awHome,
|
|
130
|
-
'.aw_registry/
|
|
131
|
+
'.aw_registry/platform/core/skills/aw-plan/SKILL.md',
|
|
131
132
|
);
|
|
132
|
-
if (existsSync(
|
|
133
|
+
if (existsSync(bridgedSkillPath)) return 'ok';
|
|
133
134
|
|
|
134
|
-
|
|
135
|
+
// Bridge can install when both ECC home and the registry skeleton exist.
|
|
136
|
+
// (The aw-plan skill source must exist somewhere — the bridge follows
|
|
137
|
+
// applyEccRegistryBridge's contract; preflight only certifies feasibility.)
|
|
138
|
+
if (existsSync(eccHome) && existsSync(awHome)) return 'will-install';
|
|
135
139
|
|
|
136
140
|
return 'failed';
|
|
137
141
|
}
|
|
@@ -176,6 +180,7 @@ function buildRecommendations({
|
|
|
176
180
|
* @param {typeof globalThis.fetch} [opts.fetchImpl]
|
|
177
181
|
* @param {string} [opts.home] Defaults to env.HOME.
|
|
178
182
|
* @param {string} [opts.awHome] Defaults to <home>/.aw.
|
|
183
|
+
* @param {string} [opts.eccHome] Defaults to <home>/.aw-ecc.
|
|
179
184
|
* @param {number} [opts.diskFreeMb] Inject for tests; production defaults to a real statfs probe.
|
|
180
185
|
* @returns {Promise<PreflightResult>}
|
|
181
186
|
*/
|
|
@@ -195,6 +200,7 @@ export async function runPreflight(opts = {}) {
|
|
|
195
200
|
// when env is the injected `{}`, opts.home should have been passed instead.
|
|
196
201
|
const home = opts.home ?? env.HOME ?? '~';
|
|
197
202
|
const awHome = opts.awHome ?? join(home, '.aw');
|
|
203
|
+
const eccHome = opts.eccHome ?? join(home, '.aw-ecc');
|
|
198
204
|
|
|
199
205
|
// 1. Token resolution.
|
|
200
206
|
const { token, source: tokenSource } = getGithubToken(env);
|
|
@@ -215,7 +221,7 @@ export async function runPreflight(opts = {}) {
|
|
|
215
221
|
const ghCliPresent = runShell(shell, 'command -v gh').ok;
|
|
216
222
|
|
|
217
223
|
// 4. Bridge status (codex-web only; other harnesses always 'ok').
|
|
218
|
-
const bridgeStatus = probeBridgeStatus(harness, awHome);
|
|
224
|
+
const bridgeStatus = probeBridgeStatus(harness, awHome, eccHome);
|
|
219
225
|
|
|
220
226
|
// 5. Disk free MB. For tests, callers inject the value directly; in
|
|
221
227
|
// production the orchestrator passes a measured number. Defaulting to
|
|
@@ -129,30 +129,30 @@ aw_c4_exit=$?
|
|
|
129
129
|
|
|
130
130
|
# Defense-in-depth: `aw c4` delegates registry sync to `aw init --silent`,
|
|
131
131
|
# which can fail to fetch in silent mode without surfacing an error. When
|
|
132
|
-
# that happens, the registry (~/.aw/.aw_registry) may be empty or incomplete
|
|
133
|
-
#
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
local
|
|
137
|
-
if [ ! -d "$
|
|
132
|
+
# that happens, the registry (~/.aw/.aw_registry) may be empty or incomplete
|
|
133
|
+
# and only the 10 hardcoded stage commands get linked. Re-run `aw pull`
|
|
134
|
+
# post-c4 if the command count looks low.
|
|
135
|
+
verify_registry_commands() {
|
|
136
|
+
local aw_cmd_dir="$HOME/.aw/.aw_registry"
|
|
137
|
+
if [ ! -d "$aw_cmd_dir" ]; then
|
|
138
138
|
echo "[aw-c4-bootstrap] registry dir missing — running aw init + pull"
|
|
139
139
|
aw init --no-integrations --silent 2>&1 | tail -3 || true
|
|
140
140
|
aw pull 2>&1 | tail -5 || true
|
|
141
141
|
return
|
|
142
142
|
fi
|
|
143
143
|
|
|
144
|
-
local
|
|
145
|
-
|
|
146
|
-
if [ "${
|
|
147
|
-
echo "[aw-c4-bootstrap] only ${
|
|
144
|
+
local cmd_count
|
|
145
|
+
cmd_count=$(find "$aw_cmd_dir" -name '*.md' -path '*/commands/*' ! -path '*/evals/*' 2>/dev/null | wc -l | tr -d ' ')
|
|
146
|
+
if [ "${cmd_count:-0}" -lt 20 ]; then
|
|
147
|
+
echo "[aw-c4-bootstrap] only ${cmd_count} registry commands found — running aw pull"
|
|
148
148
|
aw pull 2>&1 | tail -5 || true
|
|
149
149
|
local new_count
|
|
150
|
-
new_count=$(
|
|
151
|
-
echo "[aw-c4-bootstrap]
|
|
150
|
+
new_count=$(find "$aw_cmd_dir" -name '*.md' -path '*/commands/*' ! -path '*/evals/*' 2>/dev/null | wc -l | tr -d ' ')
|
|
151
|
+
echo "[aw-c4-bootstrap] registry commands: ${cmd_count} → ${new_count}"
|
|
152
152
|
else
|
|
153
|
-
echo "[aw-c4-bootstrap] registry: ${
|
|
153
|
+
echo "[aw-c4-bootstrap] registry: ${cmd_count} commands OK"
|
|
154
154
|
fi
|
|
155
155
|
}
|
|
156
|
-
|
|
156
|
+
verify_registry_commands || true
|
|
157
157
|
|
|
158
158
|
exit "$aw_c4_exit"
|
package/codex.mjs
CHANGED
|
@@ -88,19 +88,10 @@ set -euo pipefail
|
|
|
88
88
|
|
|
89
89
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
90
90
|
ROOT_DIR="$(cd "\${SCRIPT_DIR}/.." && pwd)"
|
|
91
|
-
TARGET_SCRIPT=""
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
"
|
|
95
|
-
do
|
|
96
|
-
if [[ -f "\$candidate" ]]; then
|
|
97
|
-
TARGET_SCRIPT="\$candidate"
|
|
98
|
-
break
|
|
99
|
-
fi
|
|
100
|
-
done
|
|
101
|
-
|
|
102
|
-
if [[ -z "\$TARGET_SCRIPT" ]]; then
|
|
103
|
-
echo '{"hookSpecificOutput": {"hookEventName": "SessionStart", "additionalContext": "WARNING: AW session-start hook not found at .aw_registry/aw/skills/using-aw-skills/hooks/session-start.sh. Run aw pull platform or aw init."}}'
|
|
91
|
+
TARGET_SCRIPT="\$ROOT_DIR/.aw_registry/platform/core/skills/using-aw-skills/hooks/session-start.sh"
|
|
92
|
+
|
|
93
|
+
if [[ ! -f "\$TARGET_SCRIPT" ]]; then
|
|
94
|
+
echo '{"hookSpecificOutput": {"hookEventName": "SessionStart", "additionalContext": "WARNING: AW session-start hook not found at .aw_registry/platform/core/skills/using-aw-skills/hooks/session-start.sh. Run aw pull platform or aw init."}}'
|
|
104
95
|
exit 0
|
|
105
96
|
fi
|
|
106
97
|
|