From af62504532faa35112c7f68438303e0e068c3282 Mon Sep 17 00:00:00 2001
From: Hoa Dang <70422390+hoavdc@users.noreply.github.com>
Date: Sun, 11 Oct 2026 19:45:03 +0700
Subject: [PATCH 01/24] feat(work): add scripts/build-work-plugins.mjs
---
scripts/build-work-plugins.mjs | 289 +++++++++++++++++++++++++++++++++
1 file changed, 289 insertions(+)
create mode 100644 scripts/build-work-plugins.mjs
diff --git a/scripts/build-work-plugins.mjs b/scripts/build-work-plugins.mjs
new file mode 100644
index 0000000..57e0c51
--- /dev/null
+++ b/scripts/build-work-plugins.mjs
@@ -0,0 +1,289 @@
+import { promises as fs } from "node:fs";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+
+const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
+const groupsFile = path.join(root, "config", "chatgpt-work-groups.json");
+const outRoot = path.join(root, "dist", "work");
+const packageJson = JSON.parse(await fs.readFile(path.join(root, "package.json"), "utf8"));
+const groupConfig = JSON.parse(await fs.readFile(groupsFile, "utf8"));
+const finder = await fs.readFile(path.join(root, "skill-finder.md"), "utf8");
+
+function fail(message) {
+ throw new Error(message);
+}
+
+function parseSkillFrontmatter(text, skillDirName) {
+ const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
+ if (!match) fail(`${skillDirName}/SKILL.md is missing a YAML frontmatter block.`);
+ const lines = match[1].split(/\r?\n/);
+ const values = new Map();
+ for (const line of lines) {
+ const field = line.match(/^([a-z][a-z0-9-]*):\s*(.*)$/);
+ if (field) values.set(field[1], field[2]);
+ }
+ const name = unquote(values.get("name") ?? "");
+ const description = unquote(values.get("description") ?? "");
+ if (!name || !description) fail(`${skillDirName}/SKILL.md needs name and description fields.`);
+ if (name !== skillDirName) fail(`${skillDirName}/SKILL.md name must match its directory (found ${name}).`);
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name) || name.length > 64) fail(`${skillDirName} has a non-standard skill name.`);
+ if (description.length > 1024 || /[<>]/.test(description)) fail(`${skillDirName} description must be <= 1024 characters and contain no angle brackets.`);
+ if (values.has("metadata")) fail(`${skillDirName} already has a metadata field; merge custom fields before exporting.`);
+
+ const custom = ["version", "category"].filter((key) => values.has(key));
+ const allowed = new Set(["name", "description", "license", "compatibility", "allowed-tools", ...custom]);
+ for (const key of values.keys()) if (!allowed.has(key)) fail(`${skillDirName} has unsupported top-level frontmatter field: ${key}`);
+
+ const output = lines.filter((line) => !/^(version|category):\s*/.test(line));
+ if (!values.has("license")) output.push("license: MIT");
+ if (!values.has("compatibility")) output.push('compatibility: "ChatGPT Work and Codex plugin surfaces; availability varies by product and workspace."');
+ if (custom.length) {
+ output.push("metadata:");
+ for (const key of custom) output.push(` ${key}: ${JSON.stringify(unquote(values.get(key)))}`);
+ }
+ return { name, description, text: `---\n${output.join("\n")}\n---\n${text.slice(match[0].length)}` };
+}
+
+function unquote(value) {
+ const v = value.trim();
+ if (v.length >= 2 && ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'")))) return v.slice(1, -1);
+ return v;
+}
+
+function getGroupedSkills(markdown) {
+ const groups = new Map();
+ let heading = null;
+ for (const line of markdown.split(/\r?\n/)) {
+ const h = line.match(/^## (.+)$/);
+ if (h) heading = h[1];
+ const row = line.match(/^\|\s*`(codexkit-[a-z0-9-]+)`\s*\|/);
+ if (row && groupConfig[heading]) {
+ if (!groups.has(heading)) groups.set(heading, []);
+ groups.get(heading).push(row[1]);
+ }
+ }
+ return groups;
+}
+
+function crc32(buffer) {
+ let crc = 0xffffffff;
+ for (const byte of buffer) {
+ crc ^= byte;
+ for (let bit = 0; bit < 8; bit++) crc = (crc >>> 1) ^ (crc & 1 ? 0xedb88320 : 0);
+ }
+ return (crc ^ 0xffffffff) >>> 0;
+}
+
+function dosDateTime(date = new Date()) {
+ const time = (date.getHours() << 11) | (date.getMinutes() << 5) | Math.floor(date.getSeconds() / 2);
+ const day = Math.max(1, date.getDate());
+ const dosDate = ((Math.max(1980, date.getFullYear()) - 1980) << 9) | ((date.getMonth() + 1) << 5) | day;
+ return { time, date: dosDate };
+}
+
+function makeZip(entries) {
+ const localParts = [];
+ const centralParts = [];
+ let offset = 0;
+ const { time, date } = dosDateTime();
+ for (const entry of entries) {
+ const name = Buffer.from(entry.name.replaceAll(path.sep, "/"), "utf8");
+ const data = Buffer.isBuffer(entry.data) ? entry.data : Buffer.from(entry.data);
+ const crc = crc32(data);
+ const local = Buffer.alloc(30);
+ local.writeUInt32LE(0x04034b50, 0);
+ local.writeUInt16LE(20, 4);
+ local.writeUInt16LE(0x0800, 6);
+ local.writeUInt16LE(0, 8);
+ local.writeUInt16LE(time, 10);
+ local.writeUInt16LE(date, 12);
+ local.writeUInt32LE(crc, 14);
+ local.writeUInt32LE(data.length, 18);
+ local.writeUInt32LE(data.length, 22);
+ local.writeUInt16LE(name.length, 26);
+ local.writeUInt16LE(0, 28);
+ localParts.push(local, name, data);
+
+ const central = Buffer.alloc(46);
+ central.writeUInt32LE(0x02014b50, 0);
+ central.writeUInt16LE(20, 4);
+ central.writeUInt16LE(20, 6);
+ central.writeUInt16LE(0x0800, 8);
+ central.writeUInt16LE(0, 10);
+ central.writeUInt16LE(time, 12);
+ central.writeUInt16LE(date, 14);
+ central.writeUInt32LE(crc, 16);
+ central.writeUInt32LE(data.length, 20);
+ central.writeUInt32LE(data.length, 24);
+ central.writeUInt16LE(name.length, 28);
+ central.writeUInt16LE(0, 30);
+ central.writeUInt16LE(0, 32);
+ central.writeUInt16LE(0, 34);
+ central.writeUInt16LE(0, 36);
+ central.writeUInt32LE(0, 38);
+ central.writeUInt32LE(offset, 42);
+ centralParts.push(central, name);
+ offset += local.length + name.length + data.length;
+ }
+ const centralDirectory = Buffer.concat(centralParts);
+ const end = Buffer.alloc(22);
+ end.writeUInt32LE(0x06054b50, 0);
+ end.writeUInt16LE(0, 4);
+ end.writeUInt16LE(0, 6);
+ end.writeUInt16LE(entries.length, 8);
+ end.writeUInt16LE(entries.length, 10);
+ end.writeUInt32LE(centralDirectory.length, 12);
+ end.writeUInt32LE(offset, 16);
+ end.writeUInt16LE(0, 20);
+ return Buffer.concat([...localParts, centralDirectory, end]);
+}
+
+async function listFiles(directory) {
+ const found = [];
+ for (const entry of await fs.readdir(directory, { withFileTypes: true })) {
+ const full = path.join(directory, entry.name);
+ if (entry.isDirectory()) found.push(...await listFiles(full));
+ else if (entry.isFile()) found.push(full);
+ }
+ return found.sort();
+}
+
+async function copySkill(skillId, destination) {
+ const source = path.join(root, "skills", skillId);
+ const files = await listFiles(source);
+ if (!files.some((file) => path.basename(file).toLowerCase() === "skill.md")) fail(`No SKILL.md found for ${skillId}.`);
+ for (const file of files) {
+ const relative = path.relative(source, file);
+ const target = path.join(destination, relative);
+ await fs.mkdir(path.dirname(target), { recursive: true });
+ if (path.basename(file).toLowerCase() === "skill.md") {
+ const parsed = parseSkillFrontmatter(await fs.readFile(file, "utf8"), skillId);
+ await fs.writeFile(target, parsed.text, "utf8");
+ } else {
+ await fs.copyFile(file, target);
+ }
+ }
+ return files;
+}
+
+function readZipEntryNames(buffer) {
+ let endOffset = -1;
+ for (let i = buffer.length - 22; i >= Math.max(0, buffer.length - 65557); i--) {
+ if (buffer.readUInt32LE(i) === 0x06054b50) { endOffset = i; break; }
+ }
+ if (endOffset < 0) fail("Generated ZIP is missing its end record.");
+ const count = buffer.readUInt16LE(endOffset + 10);
+ let cursor = buffer.readUInt32LE(endOffset + 16);
+ const names = [];
+ for (let i = 0; i < count; i++) {
+ if (buffer.readUInt32LE(cursor) !== 0x02014b50) fail("Generated ZIP has a malformed central directory.");
+ const nameLength = buffer.readUInt16LE(cursor + 28);
+ const extraLength = buffer.readUInt16LE(cursor + 30);
+ const commentLength = buffer.readUInt16LE(cursor + 32);
+ names.push(buffer.subarray(cursor + 46, cursor + 46 + nameLength).toString("utf8"));
+ cursor += 46 + nameLength + extraLength + commentLength;
+ }
+ return names;
+}
+
+await fs.rm(outRoot, { recursive: true, force: true });
+await fs.mkdir(outRoot, { recursive: true });
+const sourceDirs = (await fs.readdir(path.join(root, "skills"), { withFileTypes: true }))
+ .filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
+const grouped = getGroupedSkills(finder);
+const assigned = [...grouped.values()].flat();
+const duplicates = assigned.filter((id, i) => assigned.indexOf(id) !== i);
+if (duplicates.length) fail(`Skills appear in multiple Work groups: ${[...new Set(duplicates)].join(", ")}`);
+const missing = sourceDirs.filter((id) => !assigned.includes(id));
+const unknown = assigned.filter((id) => !sourceDirs.includes(id));
+if (missing.length || unknown.length) fail(`Group mismatch. Missing: ${missing.join(", ") || "none"}; unknown: ${unknown.join(", ") || "none"}`);
+for (const heading of Object.keys(groupConfig)) if (!(grouped.get(heading)?.length)) fail(`No skills found under ${heading} in skill-finder.md.`);
+
+const pluginsRoot = path.join(outRoot, "plugins");
+const pluginZipRoot = path.join(outRoot, "plugin-zips");
+const skillZipRoot = path.join(outRoot, "skill-zips");
+await Promise.all([fs.mkdir(pluginsRoot, { recursive: true }), fs.mkdir(pluginZipRoot, { recursive: true }), fs.mkdir(skillZipRoot, { recursive: true })]);
+const marketplacePlugins = [];
+const pluginSummary = [];
+
+for (const [heading, config] of Object.entries(groupConfig)) {
+ const skillIds = grouped.get(heading);
+ const pluginDir = path.join(pluginsRoot, config.slug);
+ const skillsRoot = path.join(pluginDir, "skills");
+ await fs.mkdir(skillsRoot, { recursive: true });
+ const pluginManifest = {
+ "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
+ name: `codexkit-${config.slug}`,
+ version: packageJson.version,
+ description: config.description,
+ author: { name: "Hoa Dang", url: "https://github.com/hoavdc" },
+ homepage: "https://codexkit.pages.dev",
+ repository: "https://github.com/hoavdc/CodexKit",
+ license: "MIT",
+ keywords: ["codexkit", ...config.keywords],
+ extensions: {
+ "com.openai": {
+ interface: {
+ displayName: config.displayName,
+ shortDescription: config.shortDescription,
+ longDescription: `${config.description} Includes ${skillIds.length} focused CodexKit skills. This skills-only plugin does not connect to external services.`
+ }
+ }
+ }
+ };
+ await fs.writeFile(path.join(pluginDir, "plugin.json"), `${JSON.stringify(pluginManifest, null, 2)}\n`, "utf8");
+ for (const skillId of skillIds) {
+ const skillDestination = path.join(skillsRoot, skillId);
+ const files = await copySkill(skillId, skillDestination);
+ const zipEntries = [];
+ for (const file of files) {
+ const relative = path.relative(path.join(root, "skills", skillId), file).split(path.sep).join("/");
+ const data = path.basename(file).toLowerCase() === "skill.md"
+ ? await fs.readFile(path.join(skillDestination, relative))
+ : await fs.readFile(file);
+ zipEntries.push({ name: `${skillId}/${relative}`, data });
+ }
+ const skillZip = makeZip(zipEntries);
+ const skillZipPath = path.join(skillZipRoot, `${skillId}.zip`);
+ await fs.writeFile(skillZipPath, skillZip);
+ const skillNames = readZipEntryNames(skillZip);
+ if (!skillNames.includes(`${skillId}/SKILL.md`)) fail(`Skill ZIP missing manifest: ${skillId}`);
+ }
+ const pluginFiles = await listFiles(pluginDir);
+ const pluginEntries = [];
+ for (const file of pluginFiles) {
+ pluginEntries.push({ name: path.relative(pluginDir, file).split(path.sep).join("/"), data: await fs.readFile(file) });
+ }
+ const pluginZip = makeZip(pluginEntries);
+ const pluginZipPath = path.join(pluginZipRoot, `${pluginManifest.name}.zip`);
+ await fs.writeFile(pluginZipPath, pluginZip);
+ const pluginNames = readZipEntryNames(pluginZip);
+ if (!pluginNames.includes("plugin.json") || !pluginNames.includes(`skills/${skillIds[0]}/SKILL.md`)) fail(`Plugin ZIP structure invalid: ${config.slug}`);
+
+ marketplacePlugins.push({
+ name: pluginManifest.name,
+ source: { source: "local", path: `./dist/work/plugins/${config.slug}` },
+ policy: { installation: "AVAILABLE", authentication: "ON_INSTALL" },
+ category: "Productivity"
+ });
+ pluginSummary.push({ name: pluginManifest.name, group: heading, skillCount: skillIds.length, skillIds, zip: `plugin-zips/${pluginManifest.name}.zip` });
+}
+
+const marketplace = {
+ name: "codexkit-work",
+ interface: { displayName: "CodexKit Work" },
+ plugins: marketplacePlugins
+};
+const marketplacePath = path.join(root, ".agents", "plugins", "marketplace.json");
+await fs.mkdir(path.dirname(marketplacePath), { recursive: true });
+await fs.writeFile(marketplacePath, `${JSON.stringify(marketplace, null, 2)}\n`, "utf8");
+const report = {
+ sourceVersion: packageJson.version,
+ pluginCount: pluginSummary.length,
+ skillCount: sourceDirs.length,
+ generatedAt: new Date().toISOString(),
+ plugins: pluginSummary
+};
+await fs.writeFile(path.join(outRoot, "build-report.json"), `${JSON.stringify(report, null, 2)}\n`, "utf8");
+console.log(`Built ${pluginSummary.length} plugin packages and ${sourceDirs.length} individual skill ZIPs in dist/work.`);
+console.log(`Generated repo marketplace: ${path.relative(root, marketplacePath)}`);
From f3205067e996eca25263af0014ac4d674f24993d Mon Sep 17 00:00:00 2001
From: Hoa Dang <70422390+hoavdc@users.noreply.github.com>
Date: Sun, 11 Oct 2026 19:45:11 +0700
Subject: [PATCH 02/24] feat(work): add scripts/test-work-plugins.mjs
---
scripts/test-work-plugins.mjs | 65 +++++++++++++++++++++++++++++++++++
1 file changed, 65 insertions(+)
create mode 100644 scripts/test-work-plugins.mjs
diff --git a/scripts/test-work-plugins.mjs b/scripts/test-work-plugins.mjs
new file mode 100644
index 0000000..db7e66f
--- /dev/null
+++ b/scripts/test-work-plugins.mjs
@@ -0,0 +1,65 @@
+import { promises as fs } from "node:fs";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+
+const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
+const workRoot = path.join(root, "dist", "work");
+const groups = JSON.parse(await fs.readFile(path.join(root, "tests", "skill-routing-evals.json"), "utf8"));
+const marketplace = JSON.parse(await fs.readFile(path.join(root, ".agents", "plugins", "marketplace.json"), "utf8"));
+const summary = JSON.parse(await fs.readFile(path.join(workRoot, "build-report.json"), "utf8"));
+
+function assert(condition, message) {
+ if (!condition) throw new Error(message);
+}
+
+function zipEntryNames(buffer) {
+ let end = -1;
+ for (let i = buffer.length - 22; i >= Math.max(0, buffer.length - 65_557); i -= 1) {
+ if (buffer.readUInt32LE(i) === 0x06054b50) { end = i; break; }
+ }
+ assert(end >= 0, "ZIP end record missing.");
+ const count = buffer.readUInt16LE(end + 10);
+ let cursor = buffer.readUInt32LE(end + 16);
+ const entries = [];
+ for (let i = 0; i < count; i += 1) {
+ assert(buffer.readUInt32LE(cursor) === 0x02014b50, "ZIP central directory is malformed.");
+ const nameLength = buffer.readUInt16LE(cursor + 28);
+ const extraLength = buffer.readUInt16LE(cursor + 30);
+ const commentLength = buffer.readUInt16LE(cursor + 32);
+ entries.push(buffer.subarray(cursor + 46, cursor + 46 + nameLength).toString("utf8"));
+ cursor += 46 + nameLength + extraLength + commentLength;
+ }
+ return entries;
+}
+
+const evalSlugs = groups.groups.map((group) => group.slug);
+assert(new Set(evalSlugs).size === evalSlugs.length, "Routing eval group slugs must be unique.");
+assert(groups.groups.length === marketplace.plugins.length, "Each plugin group must have routing evals.");
+assert(summary.pluginCount === marketplace.plugins.length, "Marketplace and build report plugin counts differ.");
+assert(summary.skillCount > 0, "No source skills were exported.");
+
+for (const group of groups.groups) {
+ assert(group.positive.length > 0 && group.negative.length > 0, `${group.slug} needs positive and negative prompts.`);
+ for (const prompt of [...group.positive, ...group.negative]) {
+ assert(typeof prompt === "string" && prompt.trim().length >= 20, `${group.slug} has an empty or too-short prompt.`);
+ }
+ const plugin = marketplace.plugins.find((item) => item.source.path.endsWith(`/${group.slug}`));
+ assert(plugin, `Marketplace is missing ${group.slug}.`);
+ const pluginDir = path.join(root, "dist", "work", "plugins", group.slug);
+ const manifest = JSON.parse(await fs.readFile(path.join(pluginDir, "plugin.json"), "utf8"));
+ assert(manifest.name === plugin.name, `Plugin name mismatch for ${group.slug}.`);
+ assert((await fs.stat(path.join(pluginDir, "skills"))).isDirectory(), `Plugin ${group.slug} has no skills directory.`);
+ const zipPath = path.join(workRoot, "plugin-zips", `${manifest.name}.zip`);
+ const names = zipEntryNames(await fs.readFile(zipPath));
+ assert(names.includes("plugin.json"), `Plugin ZIP missing plugin.json for ${group.slug}.`);
+ assert(names.some((name) => name.startsWith("skills/") && name.endsWith("/SKILL.md")), `Plugin ZIP has no skill for ${group.slug}.`);
+}
+
+for (const skillId of summary.plugins.flatMap((entry) => entry.skillIds)) {
+ const zipPath = path.join(workRoot, "skill-zips", `${skillId}.zip`);
+ const names = zipEntryNames(await fs.readFile(zipPath));
+ assert(names.includes(`${skillId}/SKILL.md`), `Missing single-skill ZIP for ${skillId}.`);
+}
+
+assert(groups.purpose.includes("not"), "Routing evaluation purpose must describe its limitation.");
+console.log(`Work plugin tests passed: ${marketplace.plugins.length} plugin groups, ${summary.skillCount} skills, and ${groups.groups.length} routing fixtures.`);
From 5cdb58c486df6d3349b48e5ab21f9c98922e08be Mon Sep 17 00:00:00 2001
From: Hoa Dang <70422390+hoavdc@users.noreply.github.com>
Date: Sun, 11 Oct 2026 19:45:19 +0700
Subject: [PATCH 03/24] feat(work): add config/chatgpt-work-groups.json
---
config/chatgpt-work-groups.json | 100 ++++++++++++++++++++++++++++++++
1 file changed, 100 insertions(+)
create mode 100644 config/chatgpt-work-groups.json
diff --git a/config/chatgpt-work-groups.json b/config/chatgpt-work-groups.json
new file mode 100644
index 0000000..b8eeb59
--- /dev/null
+++ b/config/chatgpt-work-groups.json
@@ -0,0 +1,100 @@
+{
+ "📬 Daily Office & Communication": {
+ "slug": "daily-office",
+ "displayName": "CodexKit: Daily Office",
+ "description": "Draft and organize everyday work communication, meeting follow-ups, briefings, reports, and presentations.",
+ "shortDescription": "Clear everyday communication and meeting workflows",
+ "keywords": ["communication", "meetings", "office", "reports"]
+ },
+ "📊 Project Management": {
+ "slug": "project-management",
+ "displayName": "CodexKit: Project Management",
+ "description": "Plan and govern projects, improve backlogs and team flow, manage risks, and coordinate delivery work.",
+ "shortDescription": "Practical project and portfolio management workflows",
+ "keywords": ["project-management", "delivery", "agile", "risk"]
+ },
+ "💰 Finance": {
+ "slug": "finance",
+ "displayName": "CodexKit: Finance",
+ "description": "Explain financial performance, coordinate close, assess statements, and build forecasts and scenarios.",
+ "shortDescription": "Finance analysis, close, and planning workflows",
+ "keywords": ["finance", "forecasting", "budget", "analysis"]
+ },
+ "⚖️ Legal & Compliance": {
+ "slug": "legal-compliance",
+ "displayName": "CodexKit: Legal & Compliance",
+ "description": "Structure legal intake, review contracts and compliance gaps, and prepare risk-aware legal work products.",
+ "shortDescription": "Legal intake, contract, and compliance workflows",
+ "keywords": ["legal", "compliance", "contracts", "risk"]
+ },
+ "👥 HR & Talent": {
+ "slug": "hr-talent",
+ "displayName": "CodexKit: HR & Talent",
+ "description": "Support hiring, onboarding, performance feedback, interviews, and talent review with structured workflows.",
+ "shortDescription": "Hiring, onboarding, and talent workflows",
+ "keywords": ["hr", "hiring", "talent", "onboarding"]
+ },
+ "📈 Strategy & Executive": {
+ "slug": "strategy-executive",
+ "displayName": "CodexKit: Strategy",
+ "description": "Turn market, operating, and performance evidence into strategy options, executive briefs, and scorecards.",
+ "shortDescription": "Strategy, executive briefs, and scorecards",
+ "keywords": ["strategy", "executive", "leadership", "analysis"]
+ },
+ "🏭 Operations & Supply Chain": {
+ "slug": "operations-supply-chain",
+ "displayName": "CodexKit: Operations",
+ "description": "Improve operating processes, supplier decisions, S&OP, KPI reviews, and continuous improvement.",
+ "shortDescription": "Operations and supply chain workflows",
+ "keywords": ["operations", "supply-chain", "process", "procurement"]
+ },
+ "🎯 Marketing & Customer Success": {
+ "slug": "marketing-customer-success",
+ "displayName": "CodexKit: Marketing & CX",
+ "description": "Plan positioning, go-to-market, campaigns, customer journeys, support, and customer success reviews.",
+ "shortDescription": "Marketing, customer success, and CX workflows",
+ "keywords": ["marketing", "customer-success", "cx", "sales"]
+ },
+ "📊 Data & Analytics": {
+ "slug": "data-analytics",
+ "displayName": "CodexKit: Data & Analytics",
+ "description": "Design experiments, analyze survey and KPI data, improve data quality, and create reliable SQL queries.",
+ "shortDescription": "Reliable analysis and data workflows",
+ "keywords": ["data", "analytics", "sql", "experiments"]
+ },
+ "🔧 Engineering & Code": {
+ "slug": "engineering-code",
+ "displayName": "CodexKit: Engineering",
+ "description": "Understand repositories, review code and APIs, investigate bugs, harden tests, and improve technical delivery.",
+ "shortDescription": "Engineering, code review, and repo workflows",
+ "keywords": ["engineering", "code", "testing", "developer-tools"]
+ },
+ "🖥️ IT & Admin": {
+ "slug": "it-admin",
+ "displayName": "CodexKit: IT & Admin",
+ "description": "Compare vendors and plan organizational change for IT and administrative operations.",
+ "shortDescription": "IT and administrative decision workflows",
+ "keywords": ["it", "admin", "vendors", "change-management"]
+ },
+ "🎓 Training & Development": {
+ "slug": "training-development",
+ "displayName": "CodexKit: Learning & Development",
+ "description": "Identify workforce learning needs and design structured workshops and development activities.",
+ "shortDescription": "Training needs and workshop workflows",
+ "keywords": ["training", "development", "learning", "workshops"]
+ },
+ "🔗 Cross-Functional": {
+ "slug": "cross-functional",
+ "displayName": "CodexKit: Cross-Functional",
+ "description": "Create decision records, business cases, policy documents, and crisis communications across teams.",
+ "shortDescription": "Decision, policy, and cross-team workflows",
+ "keywords": ["decisions", "policy", "business-case", "communication"]
+ },
+ "📐 Skill Authoring": {
+ "slug": "skill-authoring",
+ "displayName": "CodexKit: Skill Authoring",
+ "description": "Author and review focused, reusable CodexKit skills with clear triggers, workflows, and verification.",
+ "shortDescription": "Create and review reusable skills",
+ "keywords": ["skills", "authoring", "workflows", "codex"]
+ }
+}
From 335a73be0bdee2378192628781fd8cb31e7eaf15 Mon Sep 17 00:00:00 2001
From: Hoa Dang <70422390+hoavdc@users.noreply.github.com>
Date: Sun, 11 Oct 2026 19:45:27 +0700
Subject: [PATCH 04/24] feat(work): add docs/chatgpt-work.md
---
docs/chatgpt-work.md | 49 ++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 49 insertions(+)
create mode 100644 docs/chatgpt-work.md
diff --git a/docs/chatgpt-work.md b/docs/chatgpt-work.md
new file mode 100644
index 0000000..1d4962d
--- /dev/null
+++ b/docs/chatgpt-work.md
@@ -0,0 +1,49 @@
+# CodexKit for ChatGPT Work and Codex
+
+CodexKit keeps its source skills in the repository's existing format. The Work export is generated from those source files so the Codex install flow is not changed. Build it after pulling the latest repository changes:
+
+```bash
+npm run work:build
+npm run work:test
+```
+
+The build creates:
+
+- `dist/work/skill-zips/`: one portable ZIP per skill, with a single top-level skill directory.
+- `dist/work/plugin-zips/`: one ZIP per curated domain plugin.
+- `dist/work/plugins/`: unpacked plugin packages for local testing.
+- `.agents/plugins/marketplace.json`: a local marketplace manifest referencing the generated plugin directories.
+
+## Choose an install route
+
+**For one skill:** use the ChatGPT Skills interface and upload the corresponding ZIP from `dist/work/skill-zips/`. This keeps the installation small and focused. If your account does not show Skills, access may depend on plan, workspace settings, or rollout availability.
+
+**For a domain bundle in Codex:** add the repository as a plugin marketplace using Codex's supported plugin flow, then install only the plugin you need. The generated marketplace references local paths, so it is intended for a checkout on your machine and is not itself a public hosted marketplace. Restart or refresh the client if the newly installed skills do not appear.
+
+**For teams:** commit the selected unpacked plugin package to a repository or publish it through the product's supported marketplace process. Do not distribute `.env` files, tokens, or other credentials with a skills package.
+
+The installation and update behavior differs by route: a copied skill folder is editable and must be refreshed from a newer CodexKit release manually; a marketplace plugin may be managed by the host client. Avoid installing the same skills through multiple routes in the same agent, because duplicate copies make it harder to tell which version was invoked.
+
+## Keep routing predictable
+
+A skill's frontmatter description is its trigger contract. Keep it concise and concrete: state the task it handles and the useful input or desired output. Avoid vague descriptions that overlap with many unrelated skills. The generated ZIP keeps each skill's original instructions and converts CodexKit's `version` and `category` fields into standard `metadata` entries for portable consumers.
+
+CodexKit's source files already mark some skills as explicit-only in `agents/openai.yaml`. Preserve that choice for workflows where accidental invocation would be confusing or risky. For other skills, use descriptions that make the intended request easy to recognize.
+
+Use two invocation patterns deliberately:
+
+- **User-invoked:** the user asks for a named workflow or types the skill name. These skills can coordinate a multi-step flow or gather missing setup choices.
+- **Model-invoked:** the assistant can select the skill when a request clearly matches its scope. These skills should be narrow, reusable procedures that work as one part of a larger task.
+
+A user-invoked workflow can call reusable model-invoked procedures; keep the call direction one-way to avoid circular skill chains. Prefer a few focused skills that compose through clear handoffs over one skill that tries to own an entire project lifecycle.
+
+## Evaluate routing and output
+
+`tests/skill-routing-evals.json` is a lightweight, human-reviewed set of examples for each Work plugin group. It includes positive examples and near-miss prompts that should not route to that group. The checker verifies coverage and schema; it does not run an LLM or claim to measure model quality. When changing descriptions or group membership:
+
+1. Update the relevant positive and negative prompts.
+2. Run `npm run work:test`.
+3. In the target client, manually check whether representative prompts select a useful skill and whether near-misses stay out of scope.
+4. Record any observed false activations or missed activations in the PR so future edits can compare against them.
+
+When a workflow has meaningful failure modes, add examples and verification materials to the skill itself. Keep instructions short enough to follow, and use focused reference files for longer domain material.
From eb907b53e8a7e245e89cd19b765cf0a0bf7f114d Mon Sep 17 00:00:00 2001
From: Hoa Dang <70422390+hoavdc@users.noreply.github.com>
Date: Sun, 11 Oct 2026 19:45:35 +0700
Subject: [PATCH 05/24] feat(work): add tests/skill-routing-evals.json
---
tests/skill-routing-evals.json | 20 ++++++++++++++++++++
1 file changed, 20 insertions(+)
create mode 100644 tests/skill-routing-evals.json
diff --git a/tests/skill-routing-evals.json b/tests/skill-routing-evals.json
new file mode 100644
index 0000000..ce5b8a9
--- /dev/null
+++ b/tests/skill-routing-evals.json
@@ -0,0 +1,20 @@
+{
+ "schemaVersion": 1,
+ "purpose": "Human-reviewed routing fixtures; schema and coverage are tested, model behavior is not.",
+ "groups": [
+ { "slug": "daily-office", "positive": ["Summarize these meeting notes and assign owners to each follow-up."], "negative": ["Review this pull request for a race condition."] },
+ { "slug": "project-management", "positive": ["Build a risk register and delivery plan for a delayed project."], "negative": ["Explain the cash flow variance in this monthly financial statement."] },
+ { "slug": "finance", "positive": ["Explain why actual expenses are above budget and suggest corrective actions."], "negative": ["Draft a hiring scorecard for a senior engineer."] },
+ { "slug": "legal-compliance", "positive": ["Review this supplier agreement for unusual obligations and renewal risks."], "negative": ["Create a sprint plan from these backlog items."] },
+ { "slug": "hr-talent", "positive": ["Prepare structured interview questions for a product manager role."], "negative": ["Write a SQL query to compare monthly churn by cohort."] },
+ { "slug": "strategy-executive", "positive": ["Turn these market signals into strategic options for the leadership team."], "negative": ["Summarize these meeting notes and route action items."] },
+ { "slug": "operations-supply-chain", "positive": ["Assess these supplier delays and propose a recovery plan for the next S&OP review."], "negative": ["Review the wording of this employment contract."] },
+ { "slug": "marketing-customer-success", "positive": ["Create a launch message and customer success plan for this new product."], "negative": ["Calculate CPI and estimate the project cost at completion."] },
+ { "slug": "data-analytics", "positive": ["Analyze this experiment data and explain whether the conversion lift is reliable."], "negative": ["Draft a contract clause for late delivery penalties."] },
+ { "slug": "engineering-code", "positive": ["Investigate this failing test and propose a minimal code fix."], "negative": ["Prepare a quarterly business review for a customer account."] },
+ { "slug": "it-admin", "positive": ["Compare these endpoint management vendors against our IT requirements."], "negative": ["Write a customer email announcing a pricing change."] },
+ { "slug": "training-development", "positive": ["Design a workshop to help managers practice giving useful feedback."], "negative": ["Analyze the variance between forecast and actual revenue."] },
+ { "slug": "cross-functional", "positive": ["Create a decision record comparing the options and documenting owners and tradeoffs."], "negative": ["Review this pull request for a security bug."] },
+ { "slug": "skill-authoring", "positive": ["Help me write a focused reusable skill with a clear trigger and verification steps."], "negative": ["Use the existing meeting summary skill to extract decisions and actions."] }
+ ]
+}
From 588b1ad51b677537ae0f2a43d367cafb6c8179e9 Mon Sep 17 00:00:00 2001
From: Hoa Dang <70422390+hoavdc@users.noreply.github.com>
Date: Sun, 11 Oct 2026 19:45:59 +0700
Subject: [PATCH 06/24] build: add Work plugin export commands
---
package.json | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/package.json b/package.json
index ae15939..d7171e2 100644
--- a/package.json
+++ b/package.json
@@ -33,6 +33,8 @@
"dev": "npm --prefix web run dev",
"build": "npm --prefix web run build",
"lint": "npm --prefix web run lint",
- "validate": "node scripts/validate-pack.mjs"
+ "validate": "node scripts/validate-pack.mjs",
+ "work:build": "node ./scripts/build-work-plugins.mjs",
+ "work:test": "npm run work:build && node ./scripts/test-work-plugins.mjs"
}
}
From 8ad741ba293fdc87c1859cdbe1cb94852046e886 Mon Sep 17 00:00:00 2001
From: Hoa Dang <70422390+hoavdc@users.noreply.github.com>
Date: Sun, 11 Oct 2026 19:46:10 +0700
Subject: [PATCH 07/24] ci: validate Work plugin exports
---
.github/workflows/ci.yml | 3 +++
1 file changed, 3 insertions(+)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1747be0..377c058 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -34,6 +34,9 @@ jobs:
- name: Validate pack structure
run: node ./scripts/validate-pack.mjs
+ - name: Build and test ChatGPT Work plugin exports
+ run: npm run work:test
+
- name: Smoke install on Unix
if: runner.os != 'Windows'
run: |
From 33a424b5b482779e8a0142b03b4f25359a2bac2c Mon Sep 17 00:00:00 2001
From: Hoa Dang <70422390+hoavdc@users.noreply.github.com>
Date: Sun, 11 Oct 2026 19:46:18 +0700
Subject: [PATCH 08/24] docs: add ChatGPT Work export quick start
---
README.md | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/README.md b/README.md
index d44cbf3..ce7e0c1 100644
--- a/README.md
+++ b/README.md
@@ -96,6 +96,17 @@ If the install completed but the skills do not appear:
2. Check that the skills were copied into `%USERPROFILE%\.agents\skills` on Windows or `$HOME/.agents/skills` on macOS/Linux.
3. In Codex, use `/skills` to verify discovery.
+### ChatGPT Work and plugin exports
+
+CodexKit also builds portable skill ZIPs and curated Work plugin packages from the same source skills. This export leaves the Codex install folders unchanged.
+
+```bash
+npm run work:build
+npm run work:test
+```
+
+See [ChatGPT Work and Codex plugin guide](./docs/chatgpt-work.md) for single-skill upload, local marketplace setup, routing design, and manual activation checks. Product access and install options vary by plan, workspace settings, and client surface.
+
### 2. Validate the pack
```bash
From 9c06fd9436d47732242de7d4e8d5b82c324dcaf0 Mon Sep 17 00:00:00 2001
From: Hoa Dang <70422390+hoavdc@users.noreply.github.com>
Date: Sun, 11 Oct 2026 19:46:32 +0700
Subject: [PATCH 09/24] docs: clarify skill invocation contract in template
---
skills/codexkit-skill-template/SKILL.md | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/skills/codexkit-skill-template/SKILL.md b/skills/codexkit-skill-template/SKILL.md
index 17d503e..c1cf512 100644
--- a/skills/codexkit-skill-template/SKILL.md
+++ b/skills/codexkit-skill-template/SKILL.md
@@ -17,9 +17,9 @@ category: knowledge
## Procedure
-### Step 1 — Define the Intent
+### Step 1 — Define the Intent and Trigger
-Write a clear, one-sentence purpose. Answer: "After running this skill, the user will have ___."
+Write a clear, one-sentence purpose. Answer: "After running this skill, the user will have ___." Then decide whether it should be directly invoked by name, automatically selected from its description, or support both. Keep the description concrete enough to distinguish it from neighboring skills.
### Step 2 — Map the Domain
@@ -55,6 +55,7 @@ A complete skill folder with:
## Quality Criteria
- [ ] Purpose is a single, clear sentence — no compound goals
+- [ ] Invocation mode and trigger description are clear; near-miss requests remain out of scope
- [ ] Procedure steps each produce a visible deliverable
- [ ] Quality Criteria are measurable or concretely observable
- [ ] 4C Verification questions are domain-specific, not generic
From 7ab072c32d504f4caf83d4e9b47beb96b15f5ed5 Mon Sep 17 00:00:00 2001
From: Hoa Dang <70422390+hoavdc@users.noreply.github.com>
Date: Sun, 11 Oct 2026 19:47:11 +0700
Subject: [PATCH 10/24] docs: document Work skill and plugin exports
---
web/src/app/docs/installation/page.tsx | 16 ++++++++++++++++
1 file changed, 16 insertions(+)
diff --git a/web/src/app/docs/installation/page.tsx b/web/src/app/docs/installation/page.tsx
index 1af06b3..b591818 100644
--- a/web/src/app/docs/installation/page.tsx
+++ b/web/src/app/docs/installation/page.tsx
@@ -104,6 +104,22 @@ node ./scripts/validate-pack.mjs`}
+
+ Build portable single-skill ZIPs and curated domain plugins from the same source skill folders. These exports do not change the existing Codex install.
+
From 364089fd869051551093b34d61487c9cf4352f8d Mon Sep 17 00:00:00 2001
From: Hoa Dang <70422390+hoavdc@users.noreply.github.com>
Date: Sun, 11 Oct 2026 19:47:20 +0700
Subject: [PATCH 11/24] docs: explain explicit and automatic skill routing
---
web/src/app/docs/skills/page.tsx | 11 +++++++----
1 file changed, 7 insertions(+), 4 deletions(-)
diff --git a/web/src/app/docs/skills/page.tsx b/web/src/app/docs/skills/page.tsx
index 942cea9..3f17a46 100644
--- a/web/src/app/docs/skills/page.tsx
+++ b/web/src/app/docs/skills/page.tsx
@@ -78,11 +78,14 @@ export default function SkillsPage() {
ChatGPT Work and plugin exports
+
+
+ {`npm run work:build
+npm run work:test`}
+
+
+ dist/work/skill-zips/ when you want to upload one skill through the ChatGPT Skills interface.dist/work/plugins/ with a supported local marketplace workflow in Codex.Updating CodexKit
Invocation model
+ Invocation modes and routing
-
/skills or type $ in Codex CLI and IDE flows when you want to invoke a specific skill directly.description, so narrow descriptions matter more than clever naming.codexkit-cloud-delegation, codexkit-automation-designer, codexkit-contract-risk-review, and codexkit-compliance-gap-review are configured as explicit-only to reduce accidental activation./skills or type $ in Codex CLI and IDE flows to select a specific skill directly.codexkit-cloud-delegation, codexkit-automation-designer, codexkit-contract-risk-review, and codexkit-compliance-gap-review remain explicit-only to reduce accidental activation.
dist/work/skill-zips/ when you want to upload one skill through the ChatGPT Skills interface.dist/work/plugins/ with a supported local marketplace workflow in Codex.dist/work/plugins/ with a supported local marketplace workflow in ChatGPT desktop Work mode or Codex.