diff --git a/antigravity-awesome-skills/README.md b/antigravity-awesome-skills/README.md
index ab33f453..7a3382d7 100644
--- a/antigravity-awesome-skills/README.md
+++ b/antigravity-awesome-skills/README.md
@@ -79,6 +79,18 @@ npx agentic-awesome-skills --agy
The npm installer uses a shallow, release-pinned clone by default so first-run installs stay lighter than a full repository history checkout while matching the published npm package version. Use `--tag main` only when you intentionally want the current repository tip.
+### Focused single-skill install with GitHub CLI (preview)
+
+GitHub CLI can preview and install one exact skill for Copilot and other supported hosts. Use an exact `SKILL.md` path in this large, mirrored repository so the selected source is unambiguous and discovery stays fast:
+
+```bash
+gh skill preview sickn33/agentic-awesome-skills skills/brainstorming/SKILL.md
+gh skill install sickn33/agentic-awesome-skills skills/brainstorming/SKILL.md \
+ --agent github-copilot --scope user --pin v14.2.0
+```
+
+`gh skill` support is currently a GitHub CLI preview and may change. Install a focused skill or plugin surface for the job; do not use `--all` unless you intentionally want every discovered canonical and mirrored skill.
+
### Verify the install
```bash
@@ -142,7 +154,7 @@ Use the same repository, but install or invoke it in the way your host expects.
| Antigravity CLI (`agy`) | `npx agentic-awesome-skills --agy` | `/brainstorming help me plan a feature` |
| Kiro CLI | `npx agentic-awesome-skills --kiro` | `Use brainstorming to plan a feature` |
| Kiro IDE | `npx agentic-awesome-skills --path ~/.kiro/skills` | `Use @brainstorming to plan a feature` |
-| GitHub Copilot | _No installer — paste skills or rules manually_ | `Ask Copilot to use brainstorming to plan a feature` |
+| GitHub Copilot | `gh skill install sickn33/agentic-awesome-skills skills/brainstorming/SKILL.md --agent github-copilot --scope user --pin v14.2.0` (preview) | `Ask Copilot to use brainstorming to plan a feature` |
| OpenCode | `npx agentic-awesome-skills --path .agents/skills --category development,backend --risk safe,none` | `opencode run @brainstorming help me plan a feature` |
| AdaL CLI | `npx agentic-awesome-skills --path .adal/skills` | `Use brainstorming to plan a feature` |
| Custom path | `npx agentic-awesome-skills --path ./my-skills` | Depends on your tool |
diff --git a/antigravity-awesome-skills/SOURCE.md b/antigravity-awesome-skills/SOURCE.md
index a158b6e3..d5e8de18 100644
--- a/antigravity-awesome-skills/SOURCE.md
+++ b/antigravity-awesome-skills/SOURCE.md
@@ -1,8 +1,8 @@
# Source
- Repo: https://github.com/sickn33/antigravity-awesome-skills
-- Ref: 6b7501e33dd3fe4ff3715e5bd79dec2881e8b7d4
+- Ref: 314c8c499af407db8160a3eeac9043b58dc9c41a
- Remove-Paths:
-- Snapshot: 2026-07-12
+- Snapshot: 2026-07-13
- Sync-Mode: copy_skill_dirs
- Notes: vendored into playbook branch thirdparty/skill
diff --git a/antigravity-awesome-skills/apps/web-app/src/pages/Plugins.tsx b/antigravity-awesome-skills/apps/web-app/src/pages/Plugins.tsx
index 1ef8c95c..fa427b2d 100644
--- a/antigravity-awesome-skills/apps/web-app/src/pages/Plugins.tsx
+++ b/antigravity-awesome-skills/apps/web-app/src/pages/Plugins.tsx
@@ -17,6 +17,10 @@ function bundleDocUrl(): string {
return `${repoBaseUrl}/blob/main/docs/users/bundles.md`;
}
+function gettingStartedDocUrl(): string {
+ return `${repoBaseUrl}/blob/main/docs/users/getting-started.md`;
+}
+
export function Plugins(): React.ReactElement {
usePageMeta(buildPluginsMeta(specializedPlugins.length));
@@ -36,7 +40,7 @@ export function Plugins(): React.ReactElement {
Choose the focused AAS plugin for your AI coding workflow
- AAS specialized plugins are focused, domain-specific distributions of the 1,550+ skill library.
+ AAS specialized plugins are focused, domain-specific distributions of the full skill library.
Start here when you know the job: web apps, security, data analytics, documents, DevOps, QA,
OSS maintenance, mobile apps, automation, or agent and MCP systems.
@@ -55,6 +59,14 @@ export function Plugins(): React.ReactElement {
>
Browse full skill catalog
+
+ Install one skill with GitHub CLI
+
diff --git a/antigravity-awesome-skills/apps/web-app/src/pages/__tests__/Plugins.test.tsx b/antigravity-awesome-skills/apps/web-app/src/pages/__tests__/Plugins.test.tsx
index 219f8252..9ae106d1 100644
--- a/antigravity-awesome-skills/apps/web-app/src/pages/__tests__/Plugins.test.tsx
+++ b/antigravity-awesome-skills/apps/web-app/src/pages/__tests__/Plugins.test.tsx
@@ -11,6 +11,12 @@ describe('Plugins', () => {
expect(screen.getByText('AAS Web App Builder')).toBeInTheDocument();
expect(screen.getByText('AAS Security Engineer')).toBeInTheDocument();
expect(screen.getByText('AAS Marketing, SEO & Growth')).toBeInTheDocument();
+ expect(screen.getByText(/distributions of the full skill library/i)).toBeInTheDocument();
+ expect(screen.queryByText(/1,550\+/i)).not.toBeInTheDocument();
+ expect(screen.getByRole('link', { name: /Install one skill with GitHub CLI/i })).toHaveAttribute(
+ 'href',
+ expect.stringContaining('docs/users/getting-started.md'),
+ );
expect(screen.getByText(/Plugins, bundles, and workflows serve different decisions/i)).toBeInTheDocument();
expect(document.title).toContain('AAS Specialized Plugins');
expect(document.querySelector('meta[name="description"]')).toHaveAttribute(
diff --git a/antigravity-awesome-skills/docs/users/getting-started.md b/antigravity-awesome-skills/docs/users/getting-started.md
index 1def9788..e53a62f1 100644
--- a/antigravity-awesome-skills/docs/users/getting-started.md
+++ b/antigravity-awesome-skills/docs/users/getting-started.md
@@ -44,6 +44,16 @@ If you see a 404 error, use: `npx github:sickn33/agentic-awesome-skills`
git clone https://github.com/sickn33/agentic-awesome-skills.git .agent/skills
```
+**Option C — one exact skill with GitHub CLI (preview):**
+
+```bash
+gh skill preview sickn33/agentic-awesome-skills skills/brainstorming/SKILL.md
+gh skill install sickn33/agentic-awesome-skills skills/brainstorming/SKILL.md \
+ --agent github-copilot --scope user --pin v14.2.0
+```
+
+GitHub CLI skill support is currently in preview. In this large repository, use an exact `SKILL.md` path to avoid ambiguous canonical/plugin mirrors and unnecessary full-tree discovery. Avoid `--all` unless you intentionally want every discovered skill.
+
### 2. Pick Your Persona
Find the bundle that matches your role (see [bundles.md](bundles.md)):
@@ -110,7 +120,7 @@ Once installed, just talk to your AI naturally.
| **Cursor** | ✅ Native | `.cursor/skills/` |
| **OpenCode** | ✅ Full Support | `.agents/skills/` (prefer reduced installs with `--risk`, `--category`, or `--tags`) |
| **AdaL CLI** | ✅ Full Support | `.adal/skills/` |
-| **Copilot** | ⚠️ Text Only | Manual copy-paste |
+| **Copilot** | ✅ Native (preview) | `gh skill install ... --agent github-copilot` at project or user scope |
---
diff --git a/antigravity-awesome-skills/docs/vietnamese/FAQ.vi.md b/antigravity-awesome-skills/docs/vietnamese/FAQ.vi.md
index 47759563..6abf6b89 100644
--- a/antigravity-awesome-skills/docs/vietnamese/FAQ.vi.md
+++ b/antigravity-awesome-skills/docs/vietnamese/FAQ.vi.md
@@ -25,7 +25,14 @@ Nó giống như việc sở hữu một thư viện - tất cả sách đều
- ✅ **Cursor** (IDE tích hợp AI)
- ✅ **Antigravity IDE**
- ✅ **OpenCode**
-- ⚠️ **GitHub Copilot** (Hỗ trợ một phần qua việc copy-paste)
+- 🧪 **GitHub Copilot** (hỗ trợ preview qua GitHub CLI `gh skill`)
+
+Với Copilot, `gh skill` hiện vẫn ở trạng thái preview. Repository lớn này có cả bản canonical và bản mirror trong plugin, vì vậy hãy dùng đường dẫn chính xác; chỉ dùng `--all` khi bạn thực sự muốn cài mọi bản được phát hiện:
+
+```bash
+gh skill preview sickn33/agentic-awesome-skills skills/brainstorming/SKILL.md
+gh skill install sickn33/agentic-awesome-skills skills/brainstorming/SKILL.md --agent github-copilot --scope user --pin v14.2.0
+```
### Những kỹ năng này có được sử dụng miễn phí không?
diff --git a/antigravity-awesome-skills/docs/vietnamese/GETTING_STARTED.vi.md b/antigravity-awesome-skills/docs/vietnamese/GETTING_STARTED.vi.md
index 9f9e0cc1..4fa72dcc 100644
--- a/antigravity-awesome-skills/docs/vietnamese/GETTING_STARTED.vi.md
+++ b/antigravity-awesome-skills/docs/vietnamese/GETTING_STARTED.vi.md
@@ -78,7 +78,14 @@ Sau khi cài đặt, bạn chỉ cần trò chuyện với AI một cách tự n
| **Cursor** | ✅ Hỗ trợ gốc | `.cursor/skills/` |
| **OpenCode** | ✅ Hỗ trợ đầy đủ | `.agents/skills/` |
| **AdaL CLI** | ✅ Hỗ trợ đầy đủ | `.adal/skills/` |
-| **Copilot** | ⚠️ Chỉ văn bản | Copy-paste thủ công |
+| **Copilot** | 🧪 Hỗ trợ preview qua `gh skill` | Dùng GitHub CLI với đường dẫn skill chính xác |
+
+> **GitHub Copilot (preview):** `gh skill` hiện vẫn ở trạng thái preview. Vì repository lớn này có cả bản canonical và bản mirror trong plugin, hãy dùng đường dẫn chính xác để tránh nhầm lẫn; chỉ dùng `--all` khi bạn thực sự muốn cài mọi bản được phát hiện:
+>
+> ```bash
+> gh skill preview sickn33/agentic-awesome-skills skills/brainstorming/SKILL.md
+> gh skill install sickn33/agentic-awesome-skills skills/brainstorming/SKILL.md --agent github-copilot --scope user --pin v14.2.0
+> ```
---
diff --git a/antigravity-awesome-skills/docs_zh-CN/README.md b/antigravity-awesome-skills/docs_zh-CN/README.md
index 902db8fa..489988a0 100644
--- a/antigravity-awesome-skills/docs_zh-CN/README.md
+++ b/antigravity-awesome-skills/docs_zh-CN/README.md
@@ -166,7 +166,7 @@ AI 代理很聪明,但仍需要**特定于任务的操作指令**。技能是
| **Antigravity** | IDE | `(Agent Mode) Use skill...` | 全局:`~/.agents/skills/` · 工作区:`.agent/skills/` |
| **Antigravity CLI (`agy`)** | CLI | `/skill-name help me...` | `~/.gemini/antigravity-cli/skills/` |
| **Cursor** | IDE | `@skill-name (in Chat)` | `.cursor/skills/` |
-| **Copilot** | Ext | `(Paste content manually)` | N/A |
+| **Copilot** | Ext(`gh skill` 预览) | `gh skill preview sickn33/agentic-awesome-skills skills/brainstorming/SKILL.md` | GitHub CLI `gh skill` |
| **OpenCode** | CLI | `opencode run @skill-name` | `.agents/skills/` |
| **AdaL CLI** | CLI | `(Auto) Skills load on-demand` | `.adal/skills/` |
@@ -233,11 +233,13 @@ Codex 插件通过仓库本地插件入口指向相同的精选 `skills/` 树,
| Antigravity CLI (`agy`) | `npx agentic-awesome-skills --agy` | `/brainstorming help me plan a feature` |
| Kiro CLI | `npx agentic-awesome-skills --kiro` | `Use brainstorming to plan a feature` |
| Kiro IDE | `npx agentic-awesome-skills --path ~/.kiro/skills` | `Use @brainstorming to plan a feature` |
-| GitHub Copilot | _无安装器 — 手动粘贴技能或规则_ | `Ask Copilot to use brainstorming to plan a feature` |
+| GitHub Copilot | `gh skill install sickn33/agentic-awesome-skills skills/brainstorming/SKILL.md --agent github-copilot --scope user --pin v14.2.0`(预览) | `Ask Copilot to use brainstorming to plan a feature` |
| OpenCode | `npx agentic-awesome-skills --path .agents/skills --category development,backend --risk safe,none` | `opencode run @brainstorming help me plan a feature` |
| AdaL CLI | `npx agentic-awesome-skills --path .adal/skills` | `Use brainstorming to plan a feature` |
| 自定义路径 | `npx agentic-awesome-skills --path ./my-skills` | 取决于你的工具 |
+> **GitHub Copilot(预览)**:GitHub CLI 的 `gh skill` 目前处于 preview 阶段。由于本仓库规模较大且同时包含 canonical 与 plugin 镜像,请使用精确路径,避免歧义;除非你确实要安装所有发现的副本,否则不要使用 `--all`。
+
## 按工具查看最佳技能
如果你想要比"浏览所有 1,936+ 技能"更快的答案,请从工具特定指南开始:
diff --git a/antigravity-awesome-skills/docs_zh-CN/users/getting-started.md b/antigravity-awesome-skills/docs_zh-CN/users/getting-started.md
index f79e970d..c08d2de2 100644
--- a/antigravity-awesome-skills/docs_zh-CN/users/getting-started.md
+++ b/antigravity-awesome-skills/docs_zh-CN/users/getting-started.md
@@ -107,7 +107,14 @@ git clone https://github.com/sickn33/agentic-awesome-skills.git .agent/skills
| **Cursor** | ✅ 原生支持 | `.cursor/skills/` |
| **OpenCode** | ✅ 完全支持 | `.agents/skills/`(建议用 `--risk`、`--category` 或 `--tags` 做缩小安装) |
| **AdaL CLI** | ✅ 完全支持 | `.adal/skills/` |
-| **Copilot** | ⚠️ 仅文本 | 手动复制粘贴 |
+| **Copilot** | 🧪 `gh skill` 预览支持 | 使用 GitHub CLI 的 `gh skill`,并指定精确技能路径 |
+
+> **GitHub Copilot(预览)**:`gh skill` 目前处于 preview 阶段。由于本仓库规模较大且同时包含 canonical 与 plugin 镜像,请使用精确路径,避免歧义;除非你确实要安装所有发现的副本,否则不要使用 `--all`:
+>
+> ```bash
+> gh skill preview sickn33/agentic-awesome-skills skills/brainstorming/SKILL.md
+> gh skill install sickn33/agentic-awesome-skills skills/brainstorming/SKILL.md --agent github-copilot --scope user --pin v14.2.0
+> ```
---
diff --git a/antigravity-awesome-skills/package.json b/antigravity-awesome-skills/package.json
index 67d0d5fb..15b8293f 100644
--- a/antigravity-awesome-skills/package.json
+++ b/antigravity-awesome-skills/package.json
@@ -34,6 +34,9 @@
"build": "npm run chain && npm run catalog",
"check:stale-claims": "node tools/scripts/run-python.js tools/scripts/check_stale_claims.py",
"check:live-seo": "node tools/scripts/check-live-seo-geo.js",
+ "pages:redirect-bridge": "node tools/scripts/generate-pages-redirect-bridge.js",
+ "traffic:normalize": "node tools/scripts/normalize_traffic_snapshots.js",
+ "traffic:migration-readiness": "node tools/scripts/audit_search_migration_readiness.js",
"check:warning-budget": "node tools/scripts/run-python.js tools/scripts/check_validation_warning_budget.py",
"check:readme-credits": "node tools/scripts/run-python.js tools/scripts/check_readme_credits.py",
"audit:consistency": "node tools/scripts/run-python.js tools/scripts/audit_consistency.py",
diff --git a/antigravity-awesome-skills/tools/scripts/audit_search_migration_readiness.js b/antigravity-awesome-skills/tools/scripts/audit_search_migration_readiness.js
new file mode 100644
index 00000000..d0c07596
--- /dev/null
+++ b/antigravity-awesome-skills/tools/scripts/audit_search_migration_readiness.js
@@ -0,0 +1,399 @@
+#!/usr/bin/env node
+'use strict';
+
+/*
+ * This audit deliberately uses only saved evidence. It is not a substitute for
+ * a live deploy check and must never turn a legacy-only dashboard capture into
+ * a green migration.
+ */
+
+const fs = require('fs');
+const path = require('path');
+
+const DEFAULT_LEGACY_PACKAGE = 'antigravity-awesome-skills';
+const DEFAULT_CURRENT_PACKAGE = 'agentic-awesome-skills';
+const DEFAULT_CURRENT_PAGES_URL = 'https://sickn33.github.io/agentic-awesome-skills/';
+const DEFAULT_LEGACY_PAGES_URL = 'https://sickn33.github.io/antigravity-awesome-skills/';
+const SNAPSHOT_DIRECTORY = /^\d{4}-\d{2}-\d{2}$/;
+const DEFAULT_MAX_EVIDENCE_AGE_DAYS = 7;
+const DASHBOARD_FILES = {
+ gsc: {
+ file: 'google-search-console.json',
+ hosts: new Set(['search.google.com']),
+ pathPrefix: '/search-console/',
+ primaryParam: 'resource_id',
+ },
+ bing: {
+ file: 'bing-webmaster-search-performance.json',
+ hosts: new Set(['bing.com', 'www.bing.com']),
+ pathPrefix: '/webmasters/',
+ primaryParam: 'siteUrl',
+ },
+};
+
+function normaliseUrl(value) {
+ if (typeof value !== 'string' || !value.trim()) return null;
+ try {
+ const url = new URL(value.trim());
+ if (url.protocol !== 'https:' && url.protocol !== 'http:') return null;
+ url.hash = '';
+ url.search = '';
+ return url.toString();
+ } catch (_) {
+ return null;
+ }
+}
+
+function withTrailingSlash(value) {
+ const url = normaliseUrl(value);
+ return url && url.endsWith('/') ? url : url && `${url}/`;
+}
+
+function readJson(filePath, errors, label) {
+ try {
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
+ } catch (error) {
+ errors.push(`${label}: ${error.message}`);
+ return null;
+ }
+}
+
+function parseSitemap(filePath, errors) {
+ let source;
+ try {
+ source = fs.readFileSync(filePath, 'utf8');
+ } catch (error) {
+ errors.push(`current sitemap: ${error.message}`);
+ return [];
+ }
+ const locations = [...source.matchAll(/\s*([^<\s]+)\s*<\/loc>/gi)]
+ .map((match) => withTrailingSlash(match[1]))
+ .filter(Boolean);
+ if (!locations.length) errors.push('current sitemap: contains no valid URLs');
+ return [...new Set(locations)].sort();
+}
+
+function inferLegacyPagesUrl(currentPagesUrl) {
+ if (!currentPagesUrl) return null;
+ const inferred = withTrailingSlash(currentPagesUrl.replace('/agentic-awesome-skills/', '/antigravity-awesome-skills/'));
+ return inferred && inferred !== currentPagesUrl ? inferred : null;
+}
+
+function isValidIsoDate(value) {
+ if (typeof value !== 'string' || !SNAPSHOT_DIRECTORY.test(value)) return false;
+ const [year, month, day] = value.split('-').map(Number);
+ const parsed = new Date(Date.UTC(year, month - 1, day));
+ return parsed.getUTCFullYear() === year && parsed.getUTCMonth() === month - 1 && parsed.getUTCDate() === day;
+}
+
+function readSnapshots(snapshotRoot, filename, errors) {
+ let entries;
+ try {
+ entries = fs.readdirSync(snapshotRoot, { withFileTypes: true })
+ .filter((entry) => entry.isDirectory() && isValidIsoDate(entry.name))
+ .map((entry) => entry.name)
+ .sort();
+ } catch (error) {
+ errors.push(`snapshot root: ${error.message}`);
+ return [];
+ }
+ return entries.flatMap((directory) => {
+ const filePath = path.join(snapshotRoot, directory, filename);
+ if (!fs.existsSync(filePath)) return [];
+ const data = readJson(filePath, errors, `${directory}/${filename}`);
+ return data && typeof data === 'object' ? [{ directory, filePath, data }] : [];
+ });
+}
+
+function propertyEvidence(data, dashboardConfig) {
+ const signals = [];
+ for (const [source, raw] of [
+ ['source_property', data.source_property],
+ ['property_url', data.property_url],
+ ['propertyUrl', data.propertyUrl],
+ ['site_url', data.site_url],
+ ['siteUrl', data.siteUrl],
+ ]) {
+ const value = withTrailingSlash(raw);
+ if (value) signals.push({ source, value });
+ }
+ let dashboardValid = false;
+ let dashboardValidationReason = 'missing dashboard_url';
+ if (typeof data.dashboard_url === 'string') {
+ try {
+ const dashboard = new URL(data.dashboard_url);
+ const hostValid = dashboard.protocol === 'https:' && dashboardConfig.hosts.has(dashboard.hostname.toLowerCase());
+ const pathValid = dashboard.pathname.startsWith(dashboardConfig.pathPrefix);
+ const primary = withTrailingSlash(dashboard.searchParams.get(dashboardConfig.primaryParam));
+ dashboardValid = Boolean(hostValid && pathValid && primary);
+ dashboardValidationReason = dashboardValid ? null : 'dashboard host, path, or primary property parameter is invalid';
+ if (dashboardValid) {
+ for (const param of ['resource_id', 'siteUrl']) {
+ const value = withTrailingSlash(dashboard.searchParams.get(param));
+ if (value) signals.push({ source: `dashboard_url:${param}`, value });
+ }
+ }
+ } catch (_) {
+ dashboardValidationReason = 'dashboard_url is malformed';
+ }
+ }
+ const distinct = [...new Set(signals.map((signal) => signal.value))];
+ const intendedProperty = withTrailingSlash(data.intended_property);
+ const property = distinct.length === 1 ? distinct[0] : null;
+ return {
+ property,
+ signals,
+ property_conflict: distinct.length > 1,
+ intended_property: intendedProperty,
+ intended_matches_observed: !intendedProperty || (property !== null && intendedProperty === property),
+ dashboard_valid: dashboardValid,
+ dashboard_validation_reason: dashboardValidationReason,
+ };
+}
+
+function classifyProperty(property, currentPagesUrl, legacyPagesUrl) {
+ const value = withTrailingSlash(property);
+ if (!value) return 'unknown';
+ if (currentPagesUrl && value === currentPagesUrl) return 'current';
+ if (legacyPagesUrl && value === legacyPagesUrl) return 'legacy';
+ return 'other';
+}
+
+function validMetric(value) {
+ return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
+}
+
+function captureFreshness(capturedAt, snapshotDate, asOfDate, maxAgeDays) {
+ const captured = typeof capturedAt === 'string' ? new Date(capturedAt) : null;
+ if (!captured || Number.isNaN(captured.getTime())) {
+ return { valid: false, age_days: null, reason: 'missing or invalid captured_at_utc' };
+ }
+ const capturedDate = captured.toISOString().slice(0, 10);
+ const ageDays = (Date.parse(`${asOfDate}T00:00:00Z`) - Date.parse(`${capturedDate}T00:00:00Z`)) / 86400000;
+ if (capturedDate !== snapshotDate) return { valid: false, age_days: ageDays, reason: 'capture date does not match snapshot directory' };
+ if (capturedDate > asOfDate) return { valid: false, age_days: ageDays, reason: 'capture is in the future' };
+ if (ageDays > maxAgeDays) return { valid: false, age_days: ageDays, reason: 'capture is stale' };
+ return { valid: true, age_days: ageDays, reason: null };
+}
+
+function dashboardEvidence(snapshotRoot, dashboardConfig, currentPagesUrl, legacyPagesUrl, errors, asOfDate, maxAgeDays) {
+ const evidence = readSnapshots(snapshotRoot, dashboardConfig.file, errors).map(({ directory, filePath, data }) => {
+ const property = propertyEvidence(data, dashboardConfig);
+ const signalPropertyClasses = [...new Set(property.signals.map((signal) =>
+ classifyProperty(signal.value, currentPagesUrl, legacyPagesUrl)))];
+ return {
+ snapshot: directory,
+ file: filePath,
+ status: data.status || 'unknown',
+ ...property,
+ signal_property_classes: signalPropertyClasses,
+ property_class: classifyProperty(property.property, currentPagesUrl, legacyPagesUrl),
+ captured_at_utc: data.captured_at_utc || null,
+ freshness: captureFreshness(data.captured_at_utc, directory, asOfDate, maxAgeDays),
+ metrics_complete: validMetric(data?.totals?.clicks) && validMetric(data?.totals?.impressions),
+ };
+ });
+ const current = evidence.filter((entry) => entry.status === 'success'
+ && entry.property_class === 'current'
+ && !entry.property_conflict
+ && entry.intended_matches_observed
+ && entry.dashboard_valid
+ && entry.freshness.valid
+ && entry.metrics_complete);
+ const legacy = evidence.filter((entry) => entry.status === 'success' && entry.property_class === 'legacy');
+ return {
+ status: current.length ? 'pass' : 'fail',
+ current_evidence: current,
+ rejected_current_evidence: evidence.filter((entry) =>
+ (entry.property_class === 'current' || entry.signal_property_classes.includes('current')) && !current.includes(entry)),
+ legacy_evidence: legacy,
+ other_evidence: evidence.filter((entry) => entry.property_class === 'other' || entry.property_class === 'unknown'),
+ message: current.length
+ ? 'Fresh, schema-complete saved current-property evidence is present.'
+ : 'No fresh, schema-complete saved evidence for the exact current property; legacy-only or spoofed evidence is not a migration fix.',
+ };
+}
+
+function manifestPairs(payload) {
+ if (Array.isArray(payload)) return payload;
+ if (!payload || typeof payload !== 'object') return [];
+ if (Array.isArray(payload.redirects)) return payload.redirects;
+ if (Array.isArray(payload.routes)) return payload.routes;
+ if (payload.routes && typeof payload.routes === 'object') {
+ return Object.entries(payload.routes).map(([from, to]) => ({ from, to }));
+ }
+ if (payload.redirects && typeof payload.redirects === 'object') {
+ return Object.entries(payload.redirects).map(([from, to]) => ({ from, to }));
+ }
+ return [];
+}
+
+function redirectCoverage(manifestPath, currentUrls, currentPagesUrl, legacyPagesUrl, errors) {
+ if (!manifestPath) return { status: 'fail', expected: currentUrls.length, covered: 0, missing: currentUrls, message: 'No redirect manifest was supplied.' };
+ const payload = readJson(manifestPath, errors, 'redirect manifest');
+ if (!payload) return { status: 'fail', expected: currentUrls.length, covered: 0, missing: currentUrls, message: 'Redirect manifest is unreadable.' };
+ const redirects = new Map();
+ const duplicates = [];
+ const invalid = [];
+ for (const pair of manifestPairs(payload)) {
+ const from = withTrailingSlash(pair && (pair.from || pair.source || pair.legacy));
+ const to = withTrailingSlash(pair && (pair.to || pair.destination || pair.current));
+ if (!from || !to) {
+ invalid.push(pair);
+ } else if (redirects.has(from)) {
+ duplicates.push(from);
+ } else {
+ redirects.set(from, to);
+ }
+ }
+ const expectedPairs = new Map(currentUrls.map((currentUrl) => [
+ withTrailingSlash(currentUrl.replace(currentPagesUrl, legacyPagesUrl)),
+ currentUrl,
+ ]));
+ const missing = currentUrls.filter((currentUrl) => {
+ const legacyUrl = withTrailingSlash(currentUrl.replace(currentPagesUrl, legacyPagesUrl));
+ return redirects.get(legacyUrl) !== currentUrl;
+ });
+ const unexpected = [...redirects.entries()]
+ .filter(([from, to]) => expectedPairs.get(from) !== to)
+ .map(([from, to]) => ({ from, to }));
+ const exact = currentUrls.length && !missing.length && !duplicates.length && !invalid.length && !unexpected.length
+ && redirects.size === expectedPairs.size;
+ return {
+ status: exact ? 'pass' : 'fail',
+ expected: currentUrls.length,
+ covered: currentUrls.length - missing.length,
+ missing,
+ duplicates,
+ invalid,
+ unexpected,
+ message: exact ? 'Redirect manifest maps every expected legacy route exactly once and contains no extras.' : 'Redirect manifest does not exactly cover the expected legacy route set once each.',
+ };
+}
+
+function packageIdentity(currentPackagePath, legacyPackagePath, currentPackageName, errors) {
+ const current = readJson(currentPackagePath, errors, 'current package metadata');
+ const legacy = legacyPackagePath ? readJson(legacyPackagePath, errors, 'legacy package metadata') : null;
+ const currentName = currentPackageName || DEFAULT_CURRENT_PACKAGE;
+ const currentOk = Boolean(current && current.name === currentName && typeof current.version === 'string' && current.version);
+ const explicitReplacement = legacy && (legacy.replacementPackage || legacy.replacement_package);
+ const escapedName = currentName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ const deprecatedMentionsExactPackage = typeof legacy?.deprecated === 'string'
+ && new RegExp(`(^|[^a-z0-9_-])${escapedName}([^a-z0-9_-]|$)`, 'i').test(legacy.deprecated);
+ const legacyOk = Boolean(legacy
+ && legacy.name === DEFAULT_LEGACY_PACKAGE
+ && typeof legacy.version === 'string'
+ && (explicitReplacement === currentName || deprecatedMentionsExactPackage));
+ return {
+ status: currentOk && legacyOk ? 'pass' : 'fail',
+ current: current ? { name: current.name || null, version: current.version || null } : null,
+ legacy: legacy ? { name: legacy.name || null, version: legacy.version || null, deprecated: legacy.deprecated || null } : null,
+ message: currentOk && legacyOk
+ ? 'Current package identity and explicit legacy migration path are present.'
+ : 'Both a current package identity and an explicit legacy-to-current package migration path are required.',
+ };
+}
+
+function auditMigrationReadiness(options = {}) {
+ const repoRoot = path.resolve(options.repoRoot || path.resolve(__dirname, '..', '..'));
+ const errors = [];
+ const sitemapPath = path.resolve(repoRoot, options.sitemapPath || 'apps/web-app/public/sitemap.xml');
+ const snapshotRoot = path.resolve(repoRoot, options.snapshotRoot || '.codex/traffic-snapshots');
+ const currentPackagePath = path.resolve(repoRoot, options.currentPackagePath || 'package.json');
+ const asOfDate = options.asOfDate || new Date().toISOString().slice(0, 10);
+ const maxEvidenceAgeDays = Number(options.maxEvidenceAgeDays ?? DEFAULT_MAX_EVIDENCE_AGE_DAYS);
+ if (!isValidIsoDate(asOfDate)) throw new Error(`invalid as-of date: ${asOfDate}`);
+ if (!Number.isFinite(maxEvidenceAgeDays) || maxEvidenceAgeDays < 0) throw new Error('max evidence age must be a non-negative number');
+ const currentUrls = parseSitemap(sitemapPath, errors);
+ const currentPagesUrl = withTrailingSlash(options.currentPagesUrl || DEFAULT_CURRENT_PAGES_URL);
+ const legacyPagesUrl = withTrailingSlash(options.legacyPagesUrl || DEFAULT_LEGACY_PAGES_URL || inferLegacyPagesUrl(currentPagesUrl));
+ const identitiesDistinct = Boolean(currentPagesUrl && legacyPagesUrl && currentPagesUrl !== legacyPagesUrl);
+ const currentSitemap = {
+ status: identitiesDistinct && currentUrls.includes(currentPagesUrl) && currentUrls.every((url) => url.startsWith(currentPagesUrl)) ? 'pass' : 'fail',
+ current_pages_url: currentPagesUrl,
+ urls: currentUrls,
+ message: identitiesDistinct && currentUrls.includes(currentPagesUrl) && currentUrls.every((url) => url.startsWith(currentPagesUrl))
+ ? 'Current sitemap includes its root and contains only the configured current Pages identity.'
+ : 'Current sitemap is missing its root, malformed, uses identical legacy/current identities, or includes URLs outside the configured current Pages identity.',
+ };
+ const checks = {
+ identity_anchors: {
+ status: identitiesDistinct ? 'pass' : 'fail',
+ current_package_name: options.currentPackageName || DEFAULT_CURRENT_PACKAGE,
+ current_pages_url: currentPagesUrl,
+ legacy_pages_url: legacyPagesUrl,
+ message: identitiesDistinct ? 'Configured current and legacy identities are present and distinct.' : 'Current and legacy Pages identities must be explicit and distinct.',
+ },
+ current_sitemap_identity: currentSitemap,
+ google_search_console: dashboardEvidence(snapshotRoot, DASHBOARD_FILES.gsc, currentPagesUrl, legacyPagesUrl, errors, asOfDate, maxEvidenceAgeDays),
+ bing_webmaster: dashboardEvidence(snapshotRoot, DASHBOARD_FILES.bing, currentPagesUrl, legacyPagesUrl, errors, asOfDate, maxEvidenceAgeDays),
+ redirect_manifest_coverage: redirectCoverage(
+ options.redirectManifestPath && path.resolve(repoRoot, options.redirectManifestPath), currentUrls, currentPagesUrl, legacyPagesUrl, errors,
+ ),
+ npm_identities: packageIdentity(
+ currentPackagePath,
+ options.legacyPackagePath && path.resolve(repoRoot, options.legacyPackagePath),
+ options.currentPackageName,
+ errors,
+ ),
+ };
+ const failedChecks = Object.entries(checks).filter(([, check]) => check.status !== 'pass').map(([name]) => name);
+ return {
+ schema_version: 1,
+ status: failedChecks.length || errors.length ? 'not_ready' : 'ready',
+ evidence_policy: { as_of_date: asOfDate, max_age_days: maxEvidenceAgeDays },
+ identities: { current_pages_url: currentPagesUrl, legacy_pages_url: legacyPagesUrl },
+ checks,
+ third_party_state: {
+ status: 'not_assessed',
+ message: 'Third-party directories are outside this local audit. A request or partial third-party update is not reported as fixed.',
+ },
+ errors,
+ failed_checks: failedChecks,
+ };
+}
+
+function writeJsonAtomically(filePath, value) {
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
+ const temporary = `${filePath}.${process.pid}.${Date.now()}.tmp`;
+ try {
+ fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
+ fs.renameSync(temporary, filePath);
+ } finally {
+ if (fs.existsSync(temporary)) fs.unlinkSync(temporary);
+ }
+}
+
+function parseArgs(argv) {
+ const options = {};
+ const aliases = {
+ '--repo-root': 'repoRoot', '--snapshot-root': 'snapshotRoot', '--sitemap': 'sitemapPath',
+ '--redirect-manifest': 'redirectManifestPath', '--current-package': 'currentPackagePath',
+ '--legacy-package': 'legacyPackagePath', '--current-pages-url': 'currentPagesUrl',
+ '--legacy-pages-url': 'legacyPagesUrl', '--current-package-name': 'currentPackageName', '--output': 'outputPath',
+ '--as-of': 'asOfDate', '--max-evidence-age-days': 'maxEvidenceAgeDays',
+ };
+ for (let index = 0; index < argv.length; index += 1) {
+ const key = aliases[argv[index]];
+ if (!key || !argv[index + 1] || argv[index + 1].startsWith('--')) throw new Error(`Unknown option or missing value: ${argv[index]}`);
+ options[key] = argv[index + 1];
+ index += 1;
+ }
+ return options;
+}
+
+if (require.main === module) {
+ try {
+ const options = parseArgs(process.argv.slice(2));
+ const report = auditMigrationReadiness(options);
+ const outputPath = path.resolve(options.repoRoot || path.resolve(__dirname, '..', '..'), options.outputPath || '.codex/traffic-snapshots/migration-readiness.json');
+ writeJsonAtomically(outputPath, report);
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
+ process.exitCode = report.status === 'ready' ? 0 : 1;
+ } catch (error) {
+ process.stderr.write(`migration readiness audit failed: ${error.message}\n`);
+ process.exitCode = 2;
+ }
+}
+
+module.exports = { auditMigrationReadiness, parseArgs, redirectCoverage, writeJsonAtomically };
diff --git a/antigravity-awesome-skills/tools/scripts/generate-pages-redirect-bridge.js b/antigravity-awesome-skills/tools/scripts/generate-pages-redirect-bridge.js
new file mode 100644
index 00000000..d9ef1181
--- /dev/null
+++ b/antigravity-awesome-skills/tools/scripts/generate-pages-redirect-bridge.js
@@ -0,0 +1,293 @@
+#!/usr/bin/env node
+'use strict';
+
+const crypto = require('crypto');
+const fs = require('fs');
+const path = require('path');
+
+const REPO_ROOT = path.resolve(__dirname, '..', '..');
+const DEFAULT_SITEMAP = path.join(REPO_ROOT, 'apps', 'web-app', 'public', 'sitemap.xml');
+const DEFAULT_CURRENT_BASE = 'https://sickn33.github.io/agentic-awesome-skills/';
+const DEFAULT_LEGACY_BASE = 'https://sickn33.github.io/antigravity-awesome-skills/';
+const DEFAULT_EXPECTED_ROUTES = 46;
+const SAFE_SEGMENT = /^[A-Za-z0-9._~-]+$/;
+
+function sha256(value) {
+ return crypto.createHash('sha256').update(value).digest('hex');
+}
+
+function htmlEscape(value) {
+ return String(value)
+ .replaceAll('&', '&')
+ .replaceAll('<', '<')
+ .replaceAll('>', '>')
+ .replaceAll('"', '"')
+ .replaceAll("'", ''');
+}
+
+function xmlEscape(value) {
+ return htmlEscape(value);
+}
+
+function xmlUnescape(value) {
+ const entities = {
+ amp: '&',
+ lt: '<',
+ gt: '>',
+ quot: '"',
+ apos: "'",
+ };
+ return value.replace(/&(amp|lt|gt|quot|apos);/g, (_, entity) => entities[entity]);
+}
+
+function canonicalBase(value, label) {
+ let url;
+ try {
+ url = new URL(value);
+ } catch (_) {
+ throw new Error(`${label} must be an absolute URL`);
+ }
+ if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash) {
+ throw new Error(`${label} must be a credential-free HTTPS URL without query or fragment`);
+ }
+ if (!url.pathname.endsWith('/')) throw new Error(`${label} must end with a slash`);
+ return url;
+}
+
+function parseSitemap(source) {
+ const rawLocations = [...source.matchAll(/\s*([^<]+?)\s*<\/loc>/gi)].map((match) => xmlUnescape(match[1].trim()));
+ if (!rawLocations.length) throw new Error('sitemap contains no URLs');
+ if (new Set(rawLocations).size !== rawLocations.length) throw new Error('sitemap contains duplicate URLs');
+ return rawLocations;
+}
+
+function safeRelativeRoute(currentUrl, currentBase) {
+ if (currentUrl.protocol !== 'https:' || currentUrl.origin !== currentBase.origin || currentUrl.search || currentUrl.hash) {
+ throw new Error(`sitemap URL is outside the current HTTPS identity: ${currentUrl.toString()}`);
+ }
+ if (!currentUrl.pathname.startsWith(currentBase.pathname) || !currentUrl.pathname.endsWith('/')) {
+ throw new Error(`sitemap URL is outside the current base path or lacks a trailing slash: ${currentUrl.toString()}`);
+ }
+ const relative = currentUrl.pathname.slice(currentBase.pathname.length);
+ const segments = relative.split('/').filter(Boolean);
+ for (const segment of segments) {
+ if (segment === '.' || segment === '..' || !SAFE_SEGMENT.test(segment)) {
+ throw new Error(`sitemap URL contains an unsafe path segment: ${currentUrl.toString()}`);
+ }
+ }
+ return segments.join('/');
+}
+
+function outputRelativePath(legacyBase, relativeRoute) {
+ const baseSegments = legacyBase.pathname.split('/').filter(Boolean);
+ for (const segment of baseSegments) {
+ if (segment === '.' || segment === '..' || !SAFE_SEGMENT.test(segment)) {
+ throw new Error('legacy base contains an unsafe path segment');
+ }
+ }
+ return path.posix.join(...baseSegments, relativeRoute, 'index.html');
+}
+
+function redirectHtml(destination) {
+ const escaped = htmlEscape(destination);
+ return `
+
+
+
+
+
+
+ Agentic Awesome Skills has moved
+
+
+
+