@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.
@@ -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/aw/skills/
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 AW registry skill scan:
150
- * find <awHome>/.aw_registry/aw/skills/<skill>/SKILL.md
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 awSkillsRoot = join(awHome, '.aw_registry/aw/skills');
160
- const enumerableSkills = enumerateAwSkills(awSkillsRoot);
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 and return the deduped sorted skill names.
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 enumerateAwSkills(awSkillsRoot) {
172
- if (!existsSync(awSkillsRoot)) return [];
173
+ function enumerateRegistrySkills(platformRoot) {
174
+ if (!existsSync(platformRoot)) return [];
173
175
  const set = new Set();
174
- let skills;
176
+ let teams;
175
177
  try {
176
- skills = readdirSync(awSkillsRoot, { withFileTypes: true });
178
+ teams = readdirSync(platformRoot, { withFileTypes: true });
177
179
  } catch {
178
- // readdir on the AW skills root failed (EACCES, ENOENT). Treat as
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 skill of skills) {
183
- if (!skill.isDirectory()) continue;
184
- const skillMd = join(awSkillsRoot, skill.name, 'SKILL.md');
185
- if (existsSync(skillMd)) set.add(skill.name);
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 AW registry path.
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/aw/skills', skill, 'SKILL.md'));
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/aw');
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 should still appear.
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, AW registry-skill probe, and disk-free reporting into a single
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
- 'AW registry skills will be installed during this run from platform-docs ' +
75
- '(Codex uses ~/.aw/.aw_registry/aw/skills for stage routing).',
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 skills are not yet provisioned — aw init will create them ' +
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
- // AW skills are now owned by platform-docs under the top-level aw namespace.
127
- // If aw-plan is already present in the registry path, routing is ready.
128
- const awSkillPath = join(
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/aw/skills/aw-plan/SKILL.md',
131
+ '.aw_registry/platform/core/skills/aw-plan/SKILL.md',
131
132
  );
132
- if (existsSync(awSkillPath)) return 'ok';
133
+ if (existsSync(bridgedSkillPath)) return 'ok';
133
134
 
134
- if (existsSync(awHome)) return 'will-install';
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
- # Re-run `aw pull` post-c4 if the AW skill count looks low.
134
- verify_registry_skills() {
135
- local aw_registry_dir="$HOME/.aw/.aw_registry"
136
- local aw_skills_dir="$aw_registry_dir/aw/skills"
137
- if [ ! -d "$aw_registry_dir" ]; then
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 skill_count
145
- skill_count=$( (find "$aw_skills_dir" -name 'SKILL.md' 2>/dev/null || true) | wc -l | tr -d ' ' )
146
- if [ "${skill_count:-0}" -lt 5 ]; then
147
- echo "[aw-c4-bootstrap] only ${skill_count} AW registry skills found — running aw pull"
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=$( (find "$aw_skills_dir" -name 'SKILL.md' 2>/dev/null || true) | wc -l | tr -d ' ' )
151
- echo "[aw-c4-bootstrap] AW registry skills: ${skill_count} -> ${new_count}"
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: ${skill_count} AW skills OK"
153
+ echo "[aw-c4-bootstrap] registry: ${cmd_count} commands OK"
154
154
  fi
155
155
  }
156
- verify_registry_skills || true
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
- for candidate in \
93
- "\$ROOT_DIR/.aw/.aw_registry/aw/skills/using-aw-skills/hooks/session-start.sh" \
94
- "\$ROOT_DIR/.aw_registry/aw/skills/using-aw-skills/hooks/session-start.sh"
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