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: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index caa5e94..b2d80ac 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,7 +35,7 @@ jobs: run: | git archive --format=zip --output "codexkit-source-${GITHUB_REF_NAME}.zip" HEAD mkdir -p release/codexkit-starter-pack - cp -R skills templates workspaces playbooks automations mcp scripts release/codexkit-starter-pack/ + cp -R skills templates workspaces playbooks automations mcp scripts config docs tests release/codexkit-starter-pack/ cp README.md LICENSE NOTICE.md package.json CHANGELOG.md skill-finder.md HUONG-DAN-NHANH.md release/codexkit-starter-pack/ cp RELEASE-v*.md release/codexkit-starter-pack/ 2>/dev/null || true cp START-HERE-WINDOWS.cmd START-HERE.sh release/codexkit-starter-pack/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ef9dbc..c2fd356 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/). ## [Unreleased] +## [0.11.0] - 2026-10-11 + +### Added + +- **ChatGPT Work export**: Build portable ZIPs for all 90 skills and 14 curated domain plugins, plus a local marketplace manifest, from the existing CodexKit sources. +- **Routing coverage checks**: Add positive and near-miss fixtures for each plugin group, with validation of exported metadata, manifests, marketplace entries, and ZIP structure. +- **ChatGPT Work guide**: Document single-skill uploads, local marketplace setup, explicit and automatic invocation, manual activation checks, and differences between Codex and Work updates. + +### Changed + +- Normalize CodexKit version and category metadata into Agent Skills fields in exported `SKILL.md` files without changing the Codex source layout. +- Extend CI and release starter-pack contents to validate and ship the Work export, documentation, and test fixtures. +- Update skill-authoring guidance for portable Work exports. + ## [0.10.0] - 2026-05-15 ### Added diff --git a/README.md b/README.md index d44cbf3..eceabd0 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ CodexKit is a fresh project rebuilt around the surfaces that matter in Codex tod If you do not want to use Git commands or remember terminal steps, use the GitHub release package: 1. Open the Releases page for this repository. -2. Download `codexkit-starter-pack-v0.10.0.zip`. +2. Download `codexkit-starter-pack-v0.11.0.zip`. 3. Unzip it anywhere on your computer. 4. On Windows, double-click `START-HERE-WINDOWS.cmd`. - If Codex is not installed, the script will show installation instructions. @@ -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 @@ -264,26 +275,26 @@ See [CONTRIBUTING.md](./CONTRIBUTING.md) for the full guide. - Replace placeholder maintainer details where needed. - Add screenshots or a short demo GIF to the docs site. - Commit and push the release candidate branch or `main`. -- Tag `v0.10.0` after validating the pack and docs build. +- Tag `v0.11.0` after validating the pack, Work exports, and docs build. - Push the tag to trigger the GitHub release workflow. ## Release process -1. Update `package.json`, `web/package.json`, and `CHANGELOG.md`. -2. Run `npm run check`. -3. Commit and push the release commit. -4. Create the tag: `git tag v0.10.0` -5. Push the tag: `git push origin v0.10.0` -6. GitHub Actions publishes: - `codexkit-source-v0.10.0.zip` - `codexkit-starter-pack-v0.10.0.zip` +1. Update `package.json`, `web/package.json`, `CHANGELOG.md`, and `RELEASE-v0.11.0.md`. +2. Run `npm run check` (including `npm run work:test`). +3. Merge the validated release candidate into `main`. +4. Create the tag from the release commit: `git tag v0.11.0` +5. Push the tag: `git push origin v0.11.0` +6. GitHub Actions validates the release and publishes: + `codexkit-source-v0.11.0.zip` + `codexkit-starter-pack-v0.11.0.zip` ## Related files - [CONTRIBUTING.md](./CONTRIBUTING.md) - [SECURITY.md](./SECURITY.md) - [NOTICE.md](./NOTICE.md) -- [RELEASE-v0.10.0.md](./RELEASE-v0.10.0.md) +- [RELEASE-v0.11.0.md](./RELEASE-v0.11.0.md) - [templates/project-brief.md](./templates/project-brief.md) - [templates/release-checklist.md](./templates/release-checklist.md) - [workspaces/README.md](./workspaces/README.md) diff --git a/RELEASE-v0.11.0.md b/RELEASE-v0.11.0.md new file mode 100644 index 0000000..e075539 --- /dev/null +++ b/RELEASE-v0.11.0.md @@ -0,0 +1,39 @@ +# CodexKit v0.11.0 — ChatGPT Work exports + +> Portable Work plugin packages and skill ZIPs, with routing and packaging checks, built from the existing CodexKit skill library. + +## Summary + +v0.11.0 makes CodexKit skills easier to use across Codex and ChatGPT Work. It adds an export pipeline for individual skill ZIPs, curated domain plugins, and a local marketplace manifest while preserving the existing Codex source layout. + +## Added + +- Portable ZIP exports for all 90 skills. +- 14 curated plugin packages and a local marketplace manifest. +- Routing fixtures for each plugin group, including near-miss cases. +- Documentation for Work installation, invocation, activation checks, and update differences. + +## Improved + +- Exported `SKILL.md` files normalize CodexKit version and category metadata into Agent Skills fields. +- CI validates plugin manifests, metadata, ZIP contents, and routing fixture coverage on Ubuntu and Windows. +- Release starter packs include the Work export, documentation, and tests. + +## Validation + +The release workflow runs: + +```bash +npm run check +``` + +This validates the source pack, builds and tests the Work exports, lints the documentation app, and builds the production docs site. + +## Upgrade notes + +No migration is required. Existing Codex users can update as usual. ChatGPT Work users can use the guide at [docs/chatgpt-work.md](./docs/chatgpt-work.md) to upload a skill or configure the local marketplace. Product access and installation options depend on the account, workspace settings, and client surface. + +## Release assets + +- `codexkit-source-v0.11.0.zip` +- `codexkit-starter-pack-v0.11.0.zip` 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"] + } +} diff --git a/docs/chatgpt-work.md b/docs/chatgpt-work.md new file mode 100644 index 0000000..e4bfe63 --- /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 ChatGPT desktop Work mode or Codex, add the repository as a plugin marketplace using the 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. diff --git a/package.json b/package.json index ae15939..002960f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codexkit", - "version": "0.10.0", + "version": "0.11.0", "private": true, "repository": { "type": "git", @@ -29,10 +29,12 @@ "productivity" ], "scripts": { - "check": "node scripts/validate-pack.mjs && npm --prefix web run lint && npm --prefix web run build", + "check": "node scripts/validate-pack.mjs && npm run work:test && npm --prefix web run lint && npm --prefix web run build", "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" } } diff --git a/scripts/build-work-plugins.mjs b/scripts/build-work-plugins.mjs new file mode 100644 index 0000000..912682d --- /dev/null +++ b/scripts/build-work-plugins.mjs @@ -0,0 +1,291 @@ +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]; + if (line.startsWith("|") && groupConfig[heading]) { + const skillIds = [...line.matchAll(/`(codexkit-[a-z0-9-]+)`/g)].map((match) => match[1]); + if (skillIds.length) { + if (!groups.has(heading)) groups.set(heading, []); + groups.get(heading).push(...skillIds); + } + } + } + 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)}`); diff --git a/scripts/test-work-plugins.mjs b/scripts/test-work-plugins.mjs new file mode 100644 index 0000000..1f280fa --- /dev/null +++ b/scripts/test-work-plugins.mjs @@ -0,0 +1,78 @@ +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; +} + +function groupForSkill(plugins, skillId) { + const entry = plugins.find((plugin) => plugin.skillIds.includes(skillId)); + assert(entry, `Skill ${skillId} is missing from the build report.`); + return groups.groups.find((group) => group.slug === marketplace.plugins.find((plugin) => plugin.name === entry.name)?.source.path.split("/").at(-1))?.slug; +} + +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 pluginGroup = groupForSkill(summary.plugins, skillId); + const exportedSkill = await fs.readFile(path.join(root, "dist", "work", "plugins", pluginGroup, "skills", skillId, "SKILL.md"), "utf8"); + const frontmatter = exportedSkill.match(/^---\r?\n([\s\S]*?)\r?\n---/); + assert(Boolean(frontmatter), "Exported skill frontmatter is malformed for " + skillId + "."); + assert(/^name:\s*\S+/m.test(frontmatter[1]) && /^description:\s*\S+/m.test(frontmatter[1]), "Exported skill requires name and description for " + skillId + "."); + assert(!/^version:|^category:/m.test(frontmatter[1]), "CodexKit-only metadata was not normalized for " + skillId + "."); + assert(frontmatter[1].includes("metadata:\n version:") && frontmatter[1].includes(" category:"), "Standard metadata block missing for " + skillId + "."); + 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("model behavior is not"), "Routing evaluation must disclose that it does not measure model behavior."); +console.log(`Work plugin tests passed: ${marketplace.plugins.length} plugin groups, ${summary.skillCount} skills, and ${groups.groups.length} routing fixtures.`); 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 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."] } + ] +} diff --git a/web/package.json b/web/package.json index 327e751..45f91e4 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "codexkit-docs", - "version": "0.10.0", + "version": "0.11.0", "private": true, "description": "Documentation site for CodexKit, an open-source operating kit for engineering, high-reasoning work, and office automation.", "engines": { diff --git a/web/src/app/docs/installation/page.tsx b/web/src/app/docs/installation/page.tsx index 1af06b3..23f508c 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`} +
+

ChatGPT Work and plugin exports

+

+ Build portable single-skill ZIPs and curated domain plugins from the same source skill folders. These exports do not change the existing Codex install. +

+
+          {`npm run work:build
+npm run work:test`}
+        
+ +
+

Updating CodexKit

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() {

-

Invocation model

+

Invocation modes and routing