@aiwg/cli 2026.8.0 → 2026.8.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.
Files changed (70) hide show
  1. package/README.md +33 -0
  2. package/agentic/code/providers/capability-matrix.yaml +511 -0
  3. package/agentic/code/providers/model-capabilities.v1.json +120 -0
  4. package/agentic/code/providers/model-catalog.v1.json +96 -0
  5. package/agentic/code/providers/model-policy-evaluations.v1.json +50 -0
  6. package/agentic/code/providers/premium-model-allowlist.v1.json +36 -0
  7. package/bin/aiwg.mjs +14 -10
  8. package/dist/src/api/index.d.ts +1 -0
  9. package/dist/src/api/index.js +1 -0
  10. package/dist/src/artifacts/cli.js +2 -0
  11. package/dist/src/artifacts/types.js +4 -0
  12. package/dist/src/auth/client.js +209 -0
  13. package/dist/src/auth/config.js +38 -0
  14. package/dist/src/auth/credential-store.js +141 -0
  15. package/dist/src/auth/resource-credentials.js +25 -0
  16. package/dist/src/auth/types.js +2 -0
  17. package/dist/src/channel/manager.mjs +5 -5
  18. package/dist/src/cli/handlers/auth.js +125 -0
  19. package/dist/src/cli/handlers/help.js +1 -0
  20. package/dist/src/cli/handlers/index.js +3 -1
  21. package/dist/src/cli/handlers/install.js +42 -4
  22. package/dist/src/cli/handlers/marketplace.js +375 -122
  23. package/dist/src/cli/handlers/resource-versions.js +2 -0
  24. package/dist/src/cli/handlers/sessions.js +23 -5
  25. package/dist/src/cli/handlers/subcommands.js +10 -1
  26. package/dist/src/cli/handlers/use.js +342 -43
  27. package/dist/src/config/gitignore.js +1 -0
  28. package/dist/src/extensions/commands/definitions.js +19 -0
  29. package/dist/src/marketplace/exchange.js +602 -0
  30. package/dist/src/marketplace/provenance-types.js +19 -0
  31. package/dist/src/marketplace/provenance.js +834 -0
  32. package/dist/src/memory/canonical-context.js +342 -0
  33. package/dist/src/memory/context-pack.js +282 -0
  34. package/dist/src/memory/index.js +4 -0
  35. package/dist/src/memory/intake.js +118 -0
  36. package/dist/src/packages/adapters/git.js +79 -29
  37. package/dist/src/packages/package-discovery.js +81 -0
  38. package/dist/src/packages/package-registry.js +2 -0
  39. package/dist/src/packages/registry.js +119 -20
  40. package/dist/src/resources/resolver.js +1 -0
  41. package/dist/src/resources/web-release.d.ts +3 -1
  42. package/dist/src/resources/web-release.js +14 -6
  43. package/dist/src/serve/agentic-sandbox-fleet-client.js +213 -0
  44. package/dist/src/serve/fleet-mission-conductor.js +293 -0
  45. package/dist/src/sessions/index.js +1 -0
  46. package/dist/src/sessions/output-registration.js +338 -0
  47. package/dist/src/sessions/promotion.js +73 -2
  48. package/dist/src/sessions/repository.js +2 -1
  49. package/dist/src/update/notifier.mjs +13 -2
  50. package/package.json +8 -1
  51. package/tools/_resolve-impl.mjs +74 -0
  52. package/tools/agents/deploy-agents.mjs +962 -0
  53. package/tools/agents/providers/base.mjs +2954 -0
  54. package/tools/agents/providers/claude.mjs +711 -0
  55. package/tools/agents/providers/codex.mjs +699 -0
  56. package/tools/agents/providers/copilot.mjs +659 -0
  57. package/tools/agents/providers/cursor.mjs +714 -0
  58. package/tools/agents/providers/factory.mjs +1130 -0
  59. package/tools/agents/providers/hermes.mjs +663 -0
  60. package/tools/agents/providers/hook-capabilities.mjs +85 -0
  61. package/tools/agents/providers/model-role.mjs +56 -0
  62. package/tools/agents/providers/openclaw-translator.mjs +348 -0
  63. package/tools/agents/providers/openclaw.mjs +680 -0
  64. package/tools/agents/providers/opencode.mjs +675 -0
  65. package/tools/agents/providers/openhuman.mjs +292 -0
  66. package/tools/agents/providers/warp.mjs +413 -0
  67. package/tools/agents/providers/windsurf.mjs +748 -0
  68. package/tools/commands/deploy-prompts-codex.mjs +336 -0
  69. package/tools/plugin/package-plugins.mjs +1013 -0
  70. package/tools/skills/deploy-skills-codex.mjs +571 -0
@@ -0,0 +1,571 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Deploy Skills to Codex
4
+ *
5
+ * Transforms AIWG skills to Codex format. The codex provider
6
+ * (tools/agents/providers/codex.mjs) always invokes this with an explicit
7
+ * `--target <project>/.agents/skills` — the cross-provider canonical path
8
+ * codex-rs scans (codex-rs/core-skills/src/loader.rs).
9
+ *
10
+ * The `~/.codex/skills/` default below is the LEGACY home-dir path, deprecated
11
+ * after the #766 regression fix: writing both .agents/skills and
12
+ * ~/.codex/skills made codex list every kernel skill twice. It is retained
13
+ * only as a standalone-invocation fallback; normal `aiwg use` deploys pass
14
+ * --target and never write the legacy location.
15
+ *
16
+ * Codex Skill Format:
17
+ * - Location: <target>/<skill-name>/SKILL.md
18
+ * - YAML frontmatter: name (≤100 chars), description (≤500 chars)
19
+ * - Body: Instructions (kept on disk, not injected into context)
20
+ *
21
+ * Usage:
22
+ * node tools/skills/deploy-skills-codex.mjs [options]
23
+ *
24
+ * Options:
25
+ * --source <path> Source directory (defaults to repo root)
26
+ * --target <path> Target directory (defaults to ~/.codex/skills — LEGACY;
27
+ * orchestrator passes <project>/.agents/skills)
28
+ * --mode <type> Deployment mode: addons, sdlc, marketing, media-curator, research, or all (default)
29
+ * --dry-run Show what would be deployed without writing
30
+ * --force Overwrite existing files
31
+ */
32
+
33
+ import realFs from 'fs';
34
+ import { createRequire } from 'module';
35
+ const _require = createRequire(import.meta.url);
36
+ let fs;
37
+ try { const gfs = _require('graceful-fs'); gfs.gracefulify(realFs); fs = realFs; } catch { fs = realFs; }
38
+ import path from 'path';
39
+ import os from 'os';
40
+ import { getFrameworksForMode, normalizeDeploymentMode, skillMatchesProvider, isKernelSkill } from '../agents/providers/base.mjs';
41
+
42
+ const CODEX_SKILLS_DIR = path.join(os.homedir(), '.codex', 'skills');
43
+ const MAX_NAME_LENGTH = 100;
44
+ const MAX_DESCRIPTION_LENGTH = 500;
45
+ const LEGACY_RENAMED_SKILLS = new Set(['aiwg-mcp']);
46
+
47
+ function parseArgs() {
48
+ const args = process.argv.slice(2);
49
+ const cfg = {
50
+ source: null,
51
+ target: CODEX_SKILLS_DIR,
52
+ mode: 'all',
53
+ dryRun: false,
54
+ force: false,
55
+ copyStandardSkills: false,
56
+ };
57
+
58
+ for (let i = 0; i < args.length; i++) {
59
+ const a = args[i];
60
+ if (a === '--source' && args[i + 1]) cfg.source = path.resolve(args[++i]);
61
+ else if (a === '--target' && args[i + 1]) cfg.target = path.resolve(args[++i]);
62
+ else if (a === '--mode' && args[i + 1]) cfg.mode = String(args[++i]).toLowerCase();
63
+ else if (a === '--dry-run') cfg.dryRun = true;
64
+ else if (a === '--force') cfg.force = true;
65
+ else if (a === '--copy-all' || a === '--copy-standard-skills') cfg.copyStandardSkills = true;
66
+ }
67
+
68
+ cfg.mode = normalizeDeploymentMode(cfg.mode);
69
+ return cfg;
70
+ }
71
+
72
+ function ensureDir(d) {
73
+ if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
74
+ }
75
+
76
+ function stripWrappingQuotes(value) {
77
+ const trimmed = String(value ?? '').trim();
78
+ if (
79
+ (trimmed.startsWith('"') && trimmed.endsWith('"')) ||
80
+ (trimmed.startsWith("'") && trimmed.endsWith("'"))
81
+ ) {
82
+ return trimmed.slice(1, -1);
83
+ }
84
+ return trimmed;
85
+ }
86
+
87
+ function yamlDoubleQuoted(value) {
88
+ return String(value ?? '')
89
+ .replace(/\\/g, '\\\\')
90
+ .replace(/"/g, '\\"')
91
+ .replace(/\r?\n/g, ' ')
92
+ .trim();
93
+ }
94
+
95
+ function codexDisplayName(skillName) {
96
+ const acronyms = new Map([
97
+ ['aiwg', 'AIWG'],
98
+ ['dfir', 'DFIR'],
99
+ ['sdlc', 'SDLC'],
100
+ ['mcp', 'MCP'],
101
+ ['pr', 'PR'],
102
+ ]);
103
+
104
+ return String(skillName)
105
+ .split('-')
106
+ .filter(Boolean)
107
+ .map((part) => acronyms.get(part.toLowerCase()) || part.charAt(0).toUpperCase() + part.slice(1))
108
+ .join(' ');
109
+ }
110
+
111
+ function codexOpenAiMetadata(skill) {
112
+ return `interface:
113
+ display_name: "${yamlDoubleQuoted(codexDisplayName(skill.name))}"
114
+ short_description: "${yamlDoubleQuoted(skill.description)}"
115
+ `;
116
+ }
117
+
118
+ function resolveSkillModelPolicy(frontmatter) {
119
+ const block = frontmatter.match(/^commandHint:\s*\n((?:[ \t]+[^\n]*\n?)*)/m)?.[1] || '';
120
+ const hint = {};
121
+ for (const line of block.split('\n')) {
122
+ const match = line.trim().match(/^(model|modelRole|modelTier|modelEffort|modelRationale):\s*(.+)$/);
123
+ if (match) hint[match[1]] = stripWrappingQuotes(match[2]);
124
+ }
125
+ const legacy = String(hint.model || '').toLowerCase();
126
+ const role = hint.modelRole || (legacy === 'opus' ? 'reasoning'
127
+ : legacy === 'haiku' ? 'efficiency' : legacy ? 'coding' : null);
128
+ if (!role) return null;
129
+ return {
130
+ role,
131
+ tier: hint.modelTier || (role === 'reasoning' ? 'premium'
132
+ : role === 'efficiency' ? 'economy' : 'standard'),
133
+ effort: hint.modelEffort,
134
+ rationale: hint.modelRationale,
135
+ };
136
+ }
137
+
138
+ /**
139
+ * Find skill directories containing SKILL.md
140
+ */
141
+ function findSkillDirs(baseDir) {
142
+ if (!fs.existsSync(baseDir)) return [];
143
+
144
+ const skillDirs = [];
145
+ const entries = fs.readdirSync(baseDir, { withFileTypes: true });
146
+
147
+ for (const entry of entries) {
148
+ if (entry.isDirectory()) {
149
+ const skillPath = path.join(baseDir, entry.name, 'SKILL.md');
150
+ if (fs.existsSync(skillPath)) {
151
+ skillDirs.push(path.join(baseDir, entry.name));
152
+ }
153
+ }
154
+ }
155
+
156
+ return skillDirs;
157
+ }
158
+
159
+ /**
160
+ * Parse AIWG SKILL.md - handles both frontmatter and non-frontmatter formats
161
+ */
162
+ function parseSkillContent(content, skillName) {
163
+ // Try YAML frontmatter format first
164
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
165
+ if (fmMatch) {
166
+ const [, frontmatter, body] = fmMatch;
167
+ const metadata = {};
168
+
169
+ // Parse YAML-like frontmatter
170
+ for (const line of frontmatter.split('\n')) {
171
+ const colonIdx = line.indexOf(':');
172
+ if (colonIdx > 0) {
173
+ const key = line.slice(0, colonIdx).trim();
174
+ const value = line.slice(colonIdx + 1).trim();
175
+ metadata[key] = stripWrappingQuotes(value);
176
+ }
177
+ }
178
+
179
+ return { metadata, body, modelPolicy: resolveSkillModelPolicy(frontmatter) };
180
+ }
181
+
182
+ // Fallback: Parse non-frontmatter format (# skill-name header)
183
+ const lines = content.split('\n');
184
+ let name = skillName;
185
+ let description = '';
186
+ let bodyStartIdx = 0;
187
+
188
+ // Look for # header as name
189
+ for (let i = 0; i < lines.length; i++) {
190
+ const line = lines[i].trim();
191
+ if (line.startsWith('# ')) {
192
+ name = line.slice(2).trim();
193
+ bodyStartIdx = i + 1;
194
+ break;
195
+ }
196
+ }
197
+
198
+ // Look for first paragraph as description (skip empty lines)
199
+ for (let i = bodyStartIdx; i < lines.length; i++) {
200
+ const line = lines[i].trim();
201
+ if (
202
+ line &&
203
+ !line.endsWith(':') &&
204
+ !line.startsWith('#') &&
205
+ !line.startsWith('-') &&
206
+ !line.startsWith('|')
207
+ ) {
208
+ description = line;
209
+ break;
210
+ }
211
+ }
212
+
213
+ // If description is too short, try next paragraph
214
+ if (description.length < 20) {
215
+ for (let i = bodyStartIdx; i < lines.length; i++) {
216
+ const line = lines[i].trim();
217
+ if (line.startsWith('## ') && line.toLowerCase().includes('purpose')) {
218
+ // Look for content after ## Purpose
219
+ for (let j = i + 1; j < lines.length && j < i + 10; j++) {
220
+ const purposeLine = lines[j].trim();
221
+ if (purposeLine && !purposeLine.startsWith('#') && !purposeLine.startsWith('-')) {
222
+ description = purposeLine;
223
+ break;
224
+ }
225
+ }
226
+ break;
227
+ }
228
+ }
229
+ }
230
+
231
+ return {
232
+ metadata: { name, description },
233
+ body: content
234
+ };
235
+ }
236
+
237
+ /**
238
+ * Transform AIWG skill to Codex format
239
+ */
240
+ function transformToCodexSkill(skillDir) {
241
+ const skillPath = path.join(skillDir, 'SKILL.md');
242
+ const skillName = path.basename(skillDir);
243
+ const content = fs.readFileSync(skillPath, 'utf8');
244
+
245
+ // Platform filtering: skip skills with explicit restrictions that exclude codex.
246
+ // Skills using platforms: [all] (the standard token) always pass this check.
247
+ if (!skillMatchesProvider(content, 'codex')) {
248
+ return null;
249
+ }
250
+
251
+ const parsed = parseSkillContent(content, skillName);
252
+
253
+ if (!parsed) {
254
+ console.warn(`Warning: Could not parse ${skillPath}`);
255
+ return null;
256
+ }
257
+
258
+ const { metadata, body, modelPolicy } = parsed;
259
+
260
+ // Validate and truncate
261
+ const name = (metadata.name || path.basename(skillDir)).slice(0, MAX_NAME_LENGTH);
262
+ let description = metadata.description || '';
263
+
264
+ // Codex REQUIRES a non-empty description — it rejects SKILL.md files that
265
+ // lack one. Fail loudly rather than silently writing `description: ""`
266
+ // (the exact regression this guard defends against).
267
+ if (!description || !String(description).trim()) {
268
+ console.error(
269
+ `ERROR: Skill '${name}' has empty/missing description in source ${skillPath}`
270
+ );
271
+ console.error(
272
+ ` Codex rejects SKILL.md files without a description field.`
273
+ );
274
+ console.error(
275
+ ` Fix the source file: add a non-empty 'description:' to the frontmatter.`
276
+ );
277
+ return null;
278
+ }
279
+
280
+ // Truncate description to 500 chars, ending at word boundary
281
+ if (description.length > MAX_DESCRIPTION_LENGTH) {
282
+ description = description.slice(0, MAX_DESCRIPTION_LENGTH - 3);
283
+ const lastSpace = description.lastIndexOf(' ');
284
+ if (lastSpace > MAX_DESCRIPTION_LENGTH - 50) {
285
+ description = description.slice(0, lastSpace);
286
+ }
287
+ description += '...';
288
+ }
289
+
290
+ // Final guard: never emit `description: ""` under any circumstance.
291
+ const quotedDescription = yamlDoubleQuoted(description);
292
+ if (!quotedDescription || !quotedDescription.trim()) {
293
+ console.error(
294
+ `ERROR: Skill '${name}' description collapsed to empty after normalization (source: ${skillPath})`
295
+ );
296
+ return null;
297
+ }
298
+
299
+ // Build Codex skill format — include platforms: [codex] so deployed skills are self-describing
300
+ const codexContent = `---
301
+ name: "${yamlDoubleQuoted(name)}"
302
+ description: "${quotedDescription}"
303
+ platforms: [codex]
304
+ ---
305
+
306
+ ${modelPolicy
307
+ ? `<!-- aiwg:model-policy role=${modelPolicy.role} tier=${modelPolicy.tier}${modelPolicy.effort ? ` effort=${modelPolicy.effort}` : ''} outcome=unsupported${modelPolicy.rationale ? ` rationale=${yamlDoubleQuoted(modelPolicy.rationale)}` : ''} -->\n\n`
308
+ : ''}${body.trim()}
309
+ `;
310
+
311
+ return {
312
+ name,
313
+ description,
314
+ content: codexContent,
315
+ metadataContent: codexOpenAiMetadata({ name, description }),
316
+ sourcePath: skillPath,
317
+ modelPolicy,
318
+ };
319
+ }
320
+
321
+ /**
322
+ * Deploy skill to Codex skills directory
323
+ */
324
+ function deploySkill(skill, targetDir, opts) {
325
+ const { force = false, dryRun = false } = opts;
326
+ const skillDir = path.join(targetDir, skill.name);
327
+ const destPath = path.join(skillDir, 'SKILL.md');
328
+ const metadataPath = path.join(skillDir, 'agents', 'openai.yaml');
329
+
330
+ // Check if skill already exists
331
+ if (fs.existsSync(destPath)) {
332
+ const existingContent = fs.readFileSync(destPath, 'utf8');
333
+ const existingMetadata = fs.existsSync(metadataPath)
334
+ ? fs.readFileSync(metadataPath, 'utf8')
335
+ : null;
336
+ if (existingContent === skill.content && existingMetadata === skill.metadataContent && !force) {
337
+ console.log(` skip (unchanged): ${skill.name}`);
338
+ return { action: 'skip', reason: 'unchanged' };
339
+ }
340
+ }
341
+
342
+ if (dryRun) {
343
+ console.log(` [dry-run] deploy: ${skill.name}`);
344
+ if (skill.modelPolicy) {
345
+ console.log(
346
+ ` model policy: unsupported (${skill.modelPolicy.role}/${skill.modelPolicy.tier}); no native field emitted`
347
+ );
348
+ }
349
+ return { action: 'deploy', reason: 'dry-run' };
350
+ }
351
+
352
+ // Create skill directory and write SKILL.md
353
+ ensureDir(skillDir);
354
+ fs.writeFileSync(destPath, skill.content, 'utf8');
355
+ ensureDir(path.dirname(metadataPath));
356
+ fs.writeFileSync(metadataPath, skill.metadataContent, 'utf8');
357
+ // Drop a marker file so future deploys can identify AIWG-managed
358
+ // skills regardless of frontmatter format (Codex strips `namespace:`
359
+ // during transform, so the SKILL.md alone isn't a reliable signal).
360
+ // Cleanup keys off this presence.
361
+ fs.writeFileSync(path.join(skillDir, '.aiwg-managed'), 'aiwg\n', 'utf8');
362
+ console.log(` deployed: ${skill.name}`);
363
+
364
+ return { action: 'deploy', reason: 'success' };
365
+ }
366
+
367
+ /**
368
+ * Get skill directories based on mode
369
+ */
370
+ function getSkillDirectories(srcRoot, mode) {
371
+ const dirs = [];
372
+
373
+ const directSkillsDir = path.join(srcRoot, 'skills');
374
+ if (fs.existsSync(directSkillsDir)) {
375
+ dirs.push({ dir: directSkillsDir, label: path.basename(srcRoot) });
376
+ }
377
+
378
+ // Addon skills
379
+ if (mode === 'addons' || mode === 'all') {
380
+ const addonsRoot = path.join(srcRoot, 'agentic', 'code', 'addons');
381
+ if (fs.existsSync(addonsRoot)) {
382
+ const addonDirs = fs.readdirSync(addonsRoot, { withFileTypes: true })
383
+ .filter(e => e.isDirectory())
384
+ .map(e => path.join(addonsRoot, e.name, 'skills'));
385
+
386
+ for (const addonSkillsDir of addonDirs) {
387
+ if (fs.existsSync(addonSkillsDir)) {
388
+ dirs.push({ dir: addonSkillsDir, label: path.basename(path.dirname(addonSkillsDir)) });
389
+ }
390
+ }
391
+ }
392
+ }
393
+
394
+ // Framework skills discovered from framework manifests/directory structure.
395
+ const frameworks = getFrameworksForMode(srcRoot, mode);
396
+ for (const framework of frameworks) {
397
+ if (framework.components.skills.exists) {
398
+ dirs.push({ dir: framework.components.skills.path, label: framework.id });
399
+ }
400
+ }
401
+
402
+ return dirs;
403
+ }
404
+
405
+ function isFullAiwgSourceRoot(srcRoot) {
406
+ return fs.existsSync(path.join(srcRoot, 'agentic', 'code', 'frameworks')) ||
407
+ fs.existsSync(path.join(srcRoot, 'agentic', 'code', 'addons')) ||
408
+ fs.existsSync(path.join(srcRoot, 'agentic', 'code', 'extensions'));
409
+ }
410
+
411
+ (async function main() {
412
+ const cfg = parseArgs();
413
+ const { source, target, mode, dryRun, force } = cfg;
414
+
415
+ // Resolve source directory
416
+ const scriptDir = path.dirname(new URL(import.meta.url).pathname);
417
+ const repoRoot = path.resolve(scriptDir, '..', '..');
418
+ const srcRoot = source || repoRoot;
419
+ const fullAiwgSourceRoot = isFullAiwgSourceRoot(srcRoot);
420
+
421
+ console.log(`Deploying skills to Codex`);
422
+ console.log(` Source: ${srcRoot}`);
423
+ console.log(` Target: ${target}`);
424
+ console.log(` Mode: ${mode}`);
425
+ if (dryRun) console.log(` [DRY RUN]`);
426
+ console.log();
427
+
428
+ // Create target directory
429
+ if (!dryRun) {
430
+ ensureDir(target);
431
+ }
432
+
433
+ // Get skill directories based on mode
434
+ const skillDirs = getSkillDirectories(srcRoot, mode);
435
+ let totalDeployed = 0;
436
+ let totalSkipped = 0;
437
+
438
+ // Honor #1217 kernel-pivot default: deploy only kernel skills unless
439
+ // the operator opts in via `--copy-all` (or `--copy-standard-skills`).
440
+ // Codex normally deploys to the project `.agents/skills/` target, so the
441
+ // kernel/standard split is enforced at filter time rather than via separate
442
+ // destination directories. The standalone legacy default remains supported.
443
+ const copyStandardSkills = cfg.copyStandardSkills === true;
444
+
445
+ // Track every AIWG-managed source skill name so we can scope post-deploy
446
+ // cleanup to skills AIWG ships — never delete user-authored or
447
+ // third-party skills sitting alongside.
448
+ //
449
+ // Two name spaces matter for cleanup: source basename (`addons/foo/skills/<name>/`)
450
+ // AND deployed name (the `name:` frontmatter field, which Codex uses as
451
+ // the target directory). Sample: source `archive-acquisition` deploys
452
+ // as `Archive Acquisition`. Track both so cleanup catches each form.
453
+ const allManagedNames = new Set();
454
+ const desiredNames = new Set();
455
+
456
+ // Pre-pass: walk every framework/addon skill directory in the source tree
457
+ // so full-root cleanup can remove stale AIWG skills. Component-scoped
458
+ // cleanup is limited below to names owned by that component, preserving
459
+ // user-authored skills alongside the generated set.
460
+ for (const { dir } of getSkillDirectories(srcRoot, 'all')) {
461
+ const allSkills = findSkillDirs(dir);
462
+ for (const s of allSkills) {
463
+ allManagedNames.add(path.basename(s));
464
+ // Also record the frontmatter `name:` since Codex uses that as the
465
+ // target dir. Best-effort — ignore parse errors.
466
+ try {
467
+ const content = fs.readFileSync(path.join(s, 'SKILL.md'), 'utf8');
468
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
469
+ if (fmMatch) {
470
+ const nameMatch = fmMatch[1].match(/^\s*name:\s*(.+?)\s*$/m);
471
+ if (nameMatch) {
472
+ allManagedNames.add(stripWrappingQuotes(nameMatch[1]));
473
+ }
474
+ }
475
+ } catch { /* ignore */ }
476
+ }
477
+ }
478
+
479
+ for (const { dir, label } of skillDirs) {
480
+ const found = findSkillDirs(dir);
481
+ if (found.length === 0) continue;
482
+
483
+ for (const s of found) allManagedNames.add(path.basename(s));
484
+
485
+ const skills = copyStandardSkills
486
+ ? found
487
+ : found.filter(s => isKernelSkill(s));
488
+ if (skills.length === 0) continue;
489
+
490
+ for (const s of skills) desiredNames.add(path.basename(s));
491
+
492
+ console.log(`\n${label} (${skills.length} skills):`);
493
+
494
+ for (const skillDir of skills) {
495
+ const skill = transformToCodexSkill(skillDir);
496
+ if (!skill) {
497
+ // transformToCodexSkill already logged the specific reason (parse
498
+ // error, missing description, platform mismatch, etc.).
499
+ console.log(` skip: ${path.basename(skillDir)} (see error above)`);
500
+ totalSkipped++;
501
+ continue;
502
+ }
503
+
504
+ const result = deploySkill(skill, target, { force, dryRun });
505
+ if (result.action === 'deploy') totalDeployed++;
506
+ else totalSkipped++;
507
+ }
508
+ }
509
+
510
+ // Post-deploy cleanup: remove AIWG-managed skills that are stale for the
511
+ // current source scope.
512
+ //
513
+ // Full AIWG-root deploys may prune any `.aiwg-managed` skill because they
514
+ // computed a complete desired set. Component-scoped deploys (`aiwg use all`
515
+ // later iterates each addon/extension after the full-root pass) must only
516
+ // prune names owned by that component. Otherwise an addon with no kernel
517
+ // skills can delete the kernel skills installed by the preceding full-root
518
+ // Codex pass, leaving `.agents/skills/` empty while `aiwg use` still exits 0.
519
+ let totalPruned = 0;
520
+ if (fs.existsSync(target)) {
521
+ const targetEntries = fs.readdirSync(target, { withFileTypes: true });
522
+ for (const entry of targetEntries) {
523
+ if (!entry.isDirectory()) continue;
524
+ const name = entry.name;
525
+ if (desiredNames.has(name)) continue;
526
+
527
+ // Known renamed AIWG skills may predate both the current source name and
528
+ // the .aiwg-managed marker. Treat only the exact historical names as
529
+ // managed so malformed legacy frontmatter cannot survive an upgrade.
530
+ let isAiwgManaged = allManagedNames.has(name) || LEGACY_RENAMED_SKILLS.has(name);
531
+ if (!isAiwgManaged && fullAiwgSourceRoot) {
532
+ // Check for the .aiwg-managed marker file (preferred — survives
533
+ // frontmatter transforms) or fall back to namespace check.
534
+ const markerFile = path.join(target, name, '.aiwg-managed');
535
+ if (fs.existsSync(markerFile)) {
536
+ isAiwgManaged = true;
537
+ } else {
538
+ const skillFile = path.join(target, name, 'SKILL.md');
539
+ if (fs.existsSync(skillFile)) {
540
+ try {
541
+ const content = fs.readFileSync(skillFile, 'utf8');
542
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
543
+ if (fmMatch) {
544
+ const fm = fmMatch[1];
545
+ if (/^\s*namespace:\s*["']?aiwg["']?\s*$/m.test(fm)) {
546
+ isAiwgManaged = true;
547
+ }
548
+ }
549
+ } catch { /* ignore unreadable; leave alone */ }
550
+ }
551
+ }
552
+ }
553
+ if (!isAiwgManaged) continue;
554
+
555
+ const full = path.join(target, name);
556
+ if (dryRun) {
557
+ console.log(` [dry-run] would prune stale skill: ${name}`);
558
+ } else {
559
+ fs.rmSync(full, { recursive: true, force: true });
560
+ }
561
+ totalPruned++;
562
+ }
563
+ }
564
+
565
+ const prunedNote = totalPruned > 0 ? `, ${totalPruned} pruned` : '';
566
+ console.log(`\nSummary: ${totalDeployed} deployed, ${totalSkipped} skipped${prunedNote}`);
567
+
568
+ if (!dryRun && totalDeployed > 0) {
569
+ console.log(`\nRestart Codex to load new skills.`);
570
+ }
571
+ })();