From 0e360ea7ca5308a8a1b259106215150f2bb82e78 Mon Sep 17 00:00:00 2001 From: Patipat Chewprecha Date: Sun, 16 Aug 2026 20:25:58 +0700 Subject: [PATCH 1/4] =?UTF-8?q?feat(codex):=20add=20native=20Codex=20skill?= =?UTF-8?q?=20integration=C2=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 30 + CLAUDE.md | 30 +- README.md | 34 +- bin/toh-cli.js | 11 + docs/README-TH.md | 33 + installer/ide-handlers/codex.js | 1171 +++++++++---------------------- installer/install.js | 115 +-- installer/status.js | 2 + installer/uninstall.js | 90 +++ package-lock.json | 4 +- package.json | 5 +- tests/codex.test.js | 325 +++++++++ tests/run.js | 13 + 13 files changed, 978 insertions(+), 885 deletions(-) create mode 100644 installer/uninstall.js create mode 100644 tests/codex.test.js create mode 100644 tests/run.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fea8d6..7bbc4f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,36 @@ All notable changes to Toh Framework will be documented in this file. +## [2.1.0] - 2026-08-16 + +### 🧠 Native Codex Skills Integration + +The Codex integration no longer simulates `/toh-*` slash commands through a giant `AGENTS.md` — Codex has no custom slash commands, so that recognition was unreliable. TOH workflows now install as **native Codex skills** (`.codex/skills//SKILL.md`), which Codex discovers from the project root and can invoke explicitly (`$toh-vibe`, `/skills`) or match implicitly from the task description. + +#### Changed + +- **Codex handler rewritten** (`installer/ide-handlers/codex.js`) - now installs one thin native skill per TOH command (14 skills), each a wrapper that points at the real workflow in `.toh/commands/*.md` and the supporting skills in `.toh/skills/` — no duplicated workflow content. Codex constraints (no subagents, no Stop hook, no model routing) are stated explicitly with sequential fallbacks. +- **`AGENTS.md` block slimmed** - the managed `` block now carries only project-level rules: identity, capabilities, the native-skills table, legacy `/toh-*` compatibility note, `.toh` runtime map, and the tiered memory protocol. The ~800-line embedded copy of every agent body is gone (Codex has no subagents to run them). +- **Codex install output** - the success box now points at `$toh-vibe` / `/skills` and `.codex/skills/` instead of implying `/toh-*` is registered. +- **Reinstall safety** - `.toh/memory/*.md` are now seeded only if absent (previously every reinstall overwrote them, contradicting the README's "without deleting your existing memory" promise); `plan.md` / `progress.md` were already protected. +- **`--quick` reinstalls are non-interactive** - an existing install under `--quick` now defaults to Quick Update instead of prompting (interactive behavior unchanged). + +#### Added + +- **Native Codex skills** - `.codex/skills/toh/SKILL.md` + one per `/toh-*` command, generated deterministically from `src/commands/*.md` frontmatter (single source of truth), with a `metadata.generator: toh-framework` marker. +- **Stale-skill cleanup** - reinstall removes previously TOH-generated skills that no longer exist, identified by the generator marker; user skills (even user skills named `toh-*`) are never touched. +- **`toh uninstall`** - new CLI command. `--ide codex` removes only TOH-managed Codex files (skills + AGENTS.md block, user content preserved); without `--ide` it also removes `.toh` and the other TOH-owned IDE resource dirs. The installer's Fresh Install cleanup uses the same Codex teardown. +- **`toh status`** - now reports `.codex/skills/` and `AGENTS.md`. +- **Test suite** - `tests/codex.test.js` (node:test, in-band runner via `tests/run.js`) covers fresh install, AGENTS.md preservation, idempotency/determinism, user-skill preservation, stale-skill removal, uninstall scoping, and SKILL.md validity with resolving references. Runs in CI (`npm test`) on Node 18 + 22. + +#### Technical + +- Agent count (**8**), skill count (**23**), command count (**14**) unchanged — Codex skills are generated wrappers, not new content. +- Synced IDE surfaces: Codex (`.codex/skills/` + `AGENTS.md` block), README.md, docs/README-TH.md, installer output, `toh status`. +- Version bumped from **2.0.0** to **2.1.0**. + +--- + ## [2.0.0] - 2026-07-14 ### 🚀 v2.0.0 Final: Single-Source Agents, Merged Harness & Tiered Memory diff --git a/CLAUDE.md b/CLAUDE.md index f48ba8b..25daa9e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -22,14 +22,16 @@ All npm scripts wrap `node bin/toh-cli.js `, which lazy-loads `installer/*. - `npm run list` — print the catalog (hardcoded and stale — see Gotchas) - `npm run status` — inspect install state (~/.claude, ./.toh, manifest.json) - `npm run bundle` — web prompt bundles into ./dist/web-bundles +- `npm test` — node:test suite (tests/, in-band runner; currently covers the Codex installer) - `npm pack --dry-run` — check exactly what ships before any packaging change ## Verification protocol -There is NO automated test suite — .github/workflows/ci.yml only smoke-tests the CLI. Verify by running for real: +`npm test` covers the Codex install/uninstall behavior; the rest is verified by running for real +(.github/workflows/ci.yml runs both on Node 18 + 22): -1. Run what you touched — install into a scratch dir, `npm run list`/`status`, `npm pack --dry-run`. - Inspect the generated output (.toh/, .claude/, .cursor/rules/, AGENTS.md, .gemini/, .agent/workflows/) — never assume a transform worked. +1. Run what you touched — `npm test`, install into a scratch dir, `npm run list`/`status`, `npm pack --dry-run`. + Inspect the generated output (.toh/, .claude/, .cursor/rules/, AGENTS.md, .codex/skills/, .gemini/, .agent/workflows/) — never assume a transform worked. 2. Coffee-Shop-Owner Test for any user-facing change: Could a coffee-shop owner use this without tech vocabulary? Does the system ever ask a question they can't answer? When it breaks, do they know what to do next? Does the output look professionally made? @@ -52,8 +54,15 @@ then 4 handlers in `installer/ide-handlers/` (plus shared.js utilities) cover th - gemini-cli.js → BOTH Gemini CLI (.gemini/: TOML commands from src/gemini-commands/, skills, GEMINI.md) and Antigravity (.agent/workflows/ from src/antigravity-workflows/); selecting gemini auto-adds antigravity. There is no antigravity.js. -- codex.js → one root AGENTS.md with frontmatter-stripped agent bodies between - TOH-FRAMEWORK-START/END markers, read from the package's src/agents/. +- codex.js → NATIVE Codex skills: one thin wrapper per command at + .codex/skills//SKILL.md (generated from src/commands/ frontmatter; each wrapper + points at .toh/commands/*.md + .toh/skills/* and states Codex constraints — no subagents, + no Stop hook, sequential TOH LOOP) + a CONCISE managed AGENTS.md block + (TOH-FRAMEWORK-START/END: identity, capabilities, skills table, legacy `/toh-*` compat + note, memory protocol). Never embed agent bodies in AGENTS.md again (pre-v2.1 behavior — + Codex has no subagents to run them). Exports uninstallCodex() (removes only + generator-marked skills + the managed block); install.js cleanExistingInstall and the + `toh uninstall` CLI command both use it. `.toh/` runtime is seeded only-if-absent. Per-IDE command divergence lives in ONE markdown source via `` (kept only for Claude Code) / `` (kept for everyone else) blocks, resolved by shared.js @@ -102,8 +111,9 @@ loop — that is why claude-code.js transforms from package src/commands, not .t (`` marker), strictly additive prompt hook to .claude/settings.json that blocks ending a session while plan.md has unchecked unblocked tasks. The other 4 IDEs run the same loop as prose — keep it self-sufficient without hooks. -- Reinstall safety: .toh/plan.md / .toh/progress.md are seeded only if absent (live loop state — - never clobber); never remove/reorder user hook entries; never overwrite a user .claude/loop.md. +- Reinstall safety: .toh/plan.md / .toh/progress.md AND the 7 .toh/memory/*.md files are + seeded only if absent (live state — never clobber); never remove/reorder user hook entries; + never overwrite a user .claude/loop.md. ## Change checklist (5-IDE parity) @@ -137,6 +147,10 @@ loop — that is why claude-code.js transforms from package src/commands, not .t 11/6/7 — trust toh-help.md's 14 / 8 / 23. Alias collision: /toh-plan and /toh-protect both claim /toh-p. - src/agents/README.md oversimplifies two transforms — installer/ide-handlers/ is authoritative. Dead/stale, do not propagate: bin/toh-npx-wrapper.js, installer/bundle.js (v1.0.0 text, *star - commands), codex.js footer GitHub URL (ArtificialWeb; the repo is wasintoh/toh-framework). + commands). (The old codex.js footer with the ArtificialWeb URL was removed in v2.1.0.) +- Tests: `npm test` runs tests/run.js — an IN-BAND node:test runner. Do not switch it to + `node --test tests/`: child-process isolation is intermittently corrupted by the dependency + stack on Node 24 ("Unable to deserialize cloned data"). Tests set TOH_QUIET=1 (install.js + silences ora on that flag — ora is the IPC corruptor). - Internal skill version strings are independent of the package version — don't "fix" them. dist/ is gitignored and absent but whitelisted — a stray local build would silently ship. diff --git a/README.md b/README.md index 128794e..e3dbe8b 100644 --- a/README.md +++ b/README.md @@ -179,6 +179,38 @@ gemini /toh-vibe Inventory management system ``` +### Codex CLI + +Codex has no custom slash commands, so TOH installs **native Codex skills** +under `.codex/skills/` — one per TOH workflow (`toh-vibe`, `toh-plan`, +`toh-ui`, `toh-dev`, `toh-design`, `toh-test`, `toh-connect`, `toh-line`, +`toh-mobile`, `toh-fix`, `toh-ship`, `toh-protect`, `toh-help`, `toh`). + +```bash +# Open the project root in Codex +codex + +# Invoke a TOH skill explicitly ($ + skill name), or browse with /skills +$toh-vibe coffee shop management system +$toh-plan build a booking app with payments + +# Or just describe the task — Codex matches the skill by its description +"Create an inventory management system" +``` + +TOH stores framework state under `.toh/` (`plan.md`, `progress.md`, +`memory/`) and project-level rules in the managed block of `AGENTS.md`. +For compatibility, typing `/toh-vibe ...` as plain text is interpreted (via +`AGENTS.md`) as a request for the matching skill — but it is **not** a native +Codex slash command. + +Uninstall (removes only TOH-managed Codex files; your own skills and +`AGENTS.md` text are kept): + +```bash +npx toh-framework uninstall --ide codex +``` + --- ## 📋 Available Commands @@ -304,7 +336,7 @@ claude -p "/toh-vibe coffee shop management system" --permission-mode acceptEdit | 🇹🇭 Thai documentation | [docs/README-TH.md](docs/README-TH.md) | | Full version history | [CHANGELOG.md](CHANGELOG.md) | | All commands + cheatsheet | run `/toh-help` in your IDE | -| Per-project guide (auto-generated) | `CLAUDE.md` / `AGENTS.md` / `GEMINI.md` / `.cursor/rules` in your project after install | +| Per-project guide (auto-generated) | `CLAUDE.md` / `AGENTS.md` / `GEMINI.md` / `.cursor/rules` / `.codex/skills` in your project after install | | The plan artifact | `.toh/plan.md` — your app's live checklist (open it anytime to see progress) | | Design contract | `DESIGN.md` at your project root — generated per project, edit it to steer the look | diff --git a/bin/toh-cli.js b/bin/toh-cli.js index d401592..d730b5f 100755 --- a/bin/toh-cli.js +++ b/bin/toh-cli.js @@ -65,6 +65,17 @@ program await list(); }); +// Uninstall command +program + .command('uninstall') + .description('Remove Toh Framework from your project (keeps user files)') + .option('-t, --target ', 'Target directory', process.cwd()) + .option('-i, --ide ', 'IDE to remove (currently: codex). Omit for full uninstall') + .action(async (options) => { + const { uninstall } = await import('../installer/uninstall.js'); + await uninstall(options); + }); + // Status command program .command('status') diff --git a/docs/README-TH.md b/docs/README-TH.md index c5417be..4aa7ee1 100644 --- a/docs/README-TH.md +++ b/docs/README-TH.md @@ -162,6 +162,39 @@ gemini /toh-vibe ระบบจัดการ inventory ``` +### Codex CLI + +Codex ไม่มี slash command แบบกำหนดเอง ดังนั้น TOH จะติดตั้ง **native Codex +skills** ไว้ที่ `.codex/skills/` — หนึ่ง skill ต่อหนึ่งเวิร์กโฟลว์ +(`toh-vibe`, `toh-plan`, `toh-ui`, `toh-dev`, `toh-design`, `toh-test`, +`toh-connect`, `toh-line`, `toh-mobile`, `toh-fix`, `toh-ship`, +`toh-protect`, `toh-help`, `toh`) + +```bash +# เปิดโฟลเดอร์โปรเจคใน Codex +codex + +# เรียก skill ตรงๆ ด้วย $ + ชื่อ skill (หรือพิมพ์ /skills เพื่อดูทั้งหมด) +$toh-vibe ระบบจัดการร้านกาแฟ +$toh-plan สร้างแอปจองห้องพร้อมชำระเงิน + +# หรือแค่บรรยายงาน — Codex จะเลือก skill จาก description ให้เอง +"สร้างระบบจัดการ inventory" +``` + +TOH เก็บ state ของ framework ไว้ที่ `.toh/` (`plan.md`, `progress.md`, +`memory/`) และกฎระดับโปรเจคไว้ใน block ที่ TOH จัดการของ `AGENTS.md` +เพื่อความเข้ากันได้แบบเดิม ถ้าพิมพ์ `/toh-vibe ...` เป็นข้อความธรรมดา +ระบบจะตีความ (ผ่าน `AGENTS.md`) ว่าเป็นการเรียก skill ที่ตรงกัน — +แต่มัน **ไม่ใช่** native slash command ของ Codex + +ถอนการติดตั้ง (ลบเฉพาะไฟล์ Codex ที่ TOH สร้าง — skill ของคุณเองและข้อความ +ใน `AGENTS.md` จะถูกเก็บไว้): + +```bash +npx toh-framework uninstall --ide codex +``` + --- ## 📋 คำสั่งทั้งหมด diff --git a/installer/ide-handlers/codex.js b/installer/ide-handlers/codex.js index 33b6930..88e0342 100644 --- a/installer/ide-handlers/codex.js +++ b/installer/ide-handlers/codex.js @@ -1,921 +1,434 @@ /** * Codex CLI IDE Handler - * Creates AGENTS.md file for Codex CLI and Codex Web - * - * Codex uses AGENTS.md as "project memory" - automatically loaded on startup + * + * Native Codex integration (v2.1.0): + * .codex/skills//SKILL.md -> native Codex skills (the canonical layer, + * discovered by Codex from the project root) + * AGENTS.md (managed block) -> concise project-level TOH rules + + * legacy `/toh-*` compatibility note + * .toh/ -> TOH runtime/state (plan, progress, memory, + * skills, commands) — owned by install.js + * + * Codex has no custom slash commands, subagents, or Stop hooks, so the old + * "simulate /toh-* via a giant AGENTS.md" approach was unreliable. Skills are + * the supported discovery/invocation mechanism; AGENTS.md now only carries + * project-level orchestration rules inside a TOH-managed marker block. */ import fs from 'fs-extra'; import path from 'path'; import { fileURLToPath } from 'url'; -import { transformCommand, renderCapabilitiesSection } from './shared.js'; +import yaml from 'js-yaml'; +import { renderCapabilitiesSection } from './shared.js'; -// Read version from package.json const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '../../package.json'), 'utf-8')); const VERSION = pkg.version; -/** - * Create memory template files for the Memory System (v1.7.0) - * Now includes architecture.md and components.md for Code Architecture Tracking - */ -async function createMemoryFiles(memoryDir, language = 'en') { - const timestamp = new Date().toISOString().split('T')[0]; +const AGENTS_BLOCK_START = ''; +const AGENTS_BLOCK_END = ''; +const AGENTS_BLOCK_RE = /[ \t]*[\s\S]*?[ \t]*\r?\n?/g; - const activeContent = language === 'th' - ? `# 🔥 Active Task\n\n## Current Focus\n[รอคำสั่งจากผู้ใช้]\n\n## In Progress\n- (ยังไม่มี)\n\n## Next Steps\n- รอคำสั่งจากผู้ใช้\n\n---\n*Last updated: ${timestamp}*\n` - : `# 🔥 Active Task\n\n## Current Focus\n[Waiting for user command]\n\n## In Progress\n- (none)\n\n## Next Steps\n- Waiting for user command\n\n---\n*Last updated: ${timestamp}*\n`; +// Generated Codex skills carry this frontmatter marker so we can tell +// TOH-managed skills apart from a user's own skills on reinstall/uninstall. +const SKILL_GENERATOR = 'toh-framework'; - const summaryContent = language === 'th' - ? `# 📋 Project Summary\n\n## Project Overview\n- Name: [ชื่อโปรเจค]\n- Tech Stack: Next.js 14, Tailwind, shadcn/ui, Zustand, Supabase\n\n## Completed Features\n- (ยังไม่มี)\n\n## Important Notes\n- ใช้ Toh Framework v${VERSION}\n\n---\n*Last updated: ${timestamp}*\n` - : `# 📋 Project Summary\n\n## Project Overview\n- Name: [Project Name]\n- Tech Stack: Next.js 14, Tailwind, shadcn/ui, Zustand, Supabase\n\n## Completed Features\n- (none)\n\n## Important Notes\n- Using Toh Framework v${VERSION}\n\n---\n*Last updated: ${timestamp}*\n`; +// Codex skill names: 1-64 chars, lowercase letters, numbers, hyphens. +const SKILL_NAME_RE = /^[a-z0-9-]{1,64}$/; - const decisionsContent = language === 'th' - ? `# 🧠 Key Decisions\n\n## Architecture Decisions\n| Date | Decision | Reason |\n|------|----------|--------|\n| ${timestamp} | ใช้ Toh Framework | AI-Orchestration Driven Development |\n\n---\n*Last updated: ${timestamp}*\n` - : `# 🧠 Key Decisions\n\n## Architecture Decisions\n| Date | Decision | Reason |\n|------|----------|--------|\n| ${timestamp} | Use Toh Framework | AI-Orchestration Driven Development |\n\n---\n*Last updated: ${timestamp}*\n`; +// ============================================================ +// Command catalog (single source: src/commands/*.md frontmatter) +// ============================================================ - // architecture.md (v1.7.0 - Code Architecture Tracking) - const architectureContent = `# 🏗️ Project Architecture +/** + * Read src/commands/*.md and return one entry per TOH command: + * { skillName, command, aliases, description, skills, file } + * Sorted by skillName for deterministic output. Throws an actionable error + * when the catalog cannot be read (the skills layer depends on it). + */ +export async function readCommandCatalog(srcDir) { + const commandsDir = path.join(srcDir, 'commands'); + if (!(await fs.pathExists(commandsDir))) { + throw new Error(`TOH command source not found: ${commandsDir} — is this a complete toh-framework package?`); + } -> Semantic overview of project structure for AI context loading -> **Update:** After any structural changes (new pages, routes, modules, services) + const files = (await fs.readdir(commandsDir)) + .filter((f) => f.endsWith('.md') && f !== 'README.md') + .sort(); + + const catalog = []; + for (const file of files) { + const raw = await fs.readFile(path.join(commandsDir, file), 'utf8'); + const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/); + if (!fmMatch) continue; // no frontmatter -> not a command definition + + let parsed; + try { + parsed = yaml.load(fmMatch[1]) || {}; + } catch (err) { + throw new Error(`Invalid YAML frontmatter in src/commands/${file}: ${err.message}`); + } ---- + const command = String(parsed.command || '').trim(); + const skillName = command.replace(/^\//, ''); + if (!SKILL_NAME_RE.test(skillName)) continue; // not a slash command -> skip + + catalog.push({ + skillName, + command, + aliases: Array.isArray(parsed.aliases) ? parsed.aliases.map(String) : [], + description: String(parsed.description || '').trim(), + skills: Array.isArray(parsed.skills) ? parsed.skills.map(String) : [], + file + }); + } -## 📁 Entry Points + if (catalog.length === 0) { + throw new Error(`No TOH commands found in ${commandsDir} — cannot generate Codex skills.`); + } + return catalog; +} -| Type | Path | Purpose | -|------|------|---------| -| Main | \`app/page.tsx\` | Landing/Home page | -| Layout | \`app/layout.tsx\` | Root layout with providers | -| API | \`app/api/\` | API routes (if any) | +// ============================================================ +// Codex skill generation +// ============================================================ +/** + * Build the SKILL.md body for one command. Deterministic: no timestamps. + * Paths are project-root relative (Codex runs from the project root). + */ +function renderSkillMd(entry) { + const triggers = [entry.command, ...entry.aliases].map((c) => `\`${c}\``).join(', '); + // Description rules (Codex): 1-1024 chars, key use case + trigger words + // front-loaded so implicit matching survives description shortening. + const description = + `${entry.description} — TOH Framework workflow (${entry.command}). ` + + `Trigger words: ${[entry.command, ...entry.aliases].join(', ')}.`.slice(0, 1024); + + const supporting = entry.skills.length + ? `2. Read every supporting skill BEFORE executing:\n${entry.skills + .map((s) => ` - \`.toh/skills/${s}/SKILL.md\``) + .join('\n')}\n3. Execute the workflow in this session, in order.` + : '2. Execute the workflow in this session, in order.'; + + return `--- +name: ${entry.skillName} +description: ${yaml.dump(description, { lineWidth: -1, noRefs: true }).trim()} +metadata: + generator: ${SKILL_GENERATOR} + version: ${VERSION} --- -## 🗂️ Core Modules - -### \`/app\` - Pages & Routes - -| Route | File | Description | Key Functions | -|-------|------|-------------|---------------| -| \`/\` | \`app/page.tsx\` | Landing page | - | - -### \`/components\` - UI Components - -| Folder | Purpose | Key Files | -|--------|---------|-----------| -| \`ui/\` | shadcn/ui components | button, card, input, etc. | -| \`layout/\` | Layout components | Navbar, Sidebar, Footer | -| \`features/\` | Feature-specific | Per feature components | +# ${entry.command} — ${entry.description} -### \`/lib\` - Utilities & Services +> Codex-native wrapper for the TOH Framework workflow ${entry.command}. +> Generated by ${SKILL_GENERATOR} — do not edit by hand; re-run +> \`npx toh-framework install --ide codex\` to update. +> All paths below are relative to the project root (the directory holding \`.codex/\`). -| File | Purpose | Key Functions | -|------|---------|---------------| -| \`lib/utils.ts\` | Utility functions | cn(), formatDate() | +## When to use ---- - -## 🔄 Data Flow Pattern +${entry.description}. Triggers: ${triggers}, or any plain-language request that matches. -User Action → Component → Zustand Store → API/Lib → Database (Supabase) - ---- +## Workflow -## 🔌 External Services +1. Read the full workflow definition: \`.toh/commands/${entry.file}\` +${supporting} -| Service | Purpose | Config Location | -|---------|---------|-----------------| -| Supabase | Backend (Auth, DB) | \`lib/supabase/\` | +If \`.toh/commands/${entry.file}\` is missing (TOH commands component not +installed), follow the supporting skills directly — they carry the same rules. ---- +## Codex constraints -## 📝 Notes +- **No subagents/teams** — where the workflow says "spawn" or "delegate", do + that work inline, one task at a time, in this session. +- **No Claude Code Stop hook** — self-enforce THE TOH LOOP: do not end the run + while \`.toh/plan.md\` has unchecked, unblocked tasks. +- **No model routing** — ignore haiku/sonnet/opus tiers mentioned in TOH docs. +- **State** — persist everything under \`.toh/\` (\`plan.md\`, \`progress.md\`, + \`memory/\`); resume = continue at the first unchecked \`[ ]\` task. -- Using Toh Framework v${VERSION} -- Architecture tracking enabled +## Legacy command text ---- -*Last updated: ${timestamp}* +If the user typed ${triggers} as text: that is this skill. \`/toh-*\` is +compatibility text interpreted via AGENTS.md, not a native Codex slash command. `; +} - // components.md (v1.7.0 - Component Registry) - const componentsContent = `# 📦 Component Registry - -> Quick reference for all project components, hooks, and utilities -> **Update:** After creating/modifying any component, hook, or utility - ---- - -## 📄 Pages - -| Route | File | Description | Key Dependencies | -|-------|------|-------------|------------------| -| \`/\` | \`app/page.tsx\` | Landing page | - | - ---- - -## 🧩 Components - -### Layout Components - -| Component | Location | Key Props | Used By | -|-----------|----------|-----------|---------| -| (none yet) | - | - | - | - -### Feature Components - -| Component | Location | Key Props | Used By | -|-----------|----------|-----------|---------| -| (none yet) | - | - | - | - ---- - -## 🪝 Custom Hooks - -| Hook | Location | Purpose | Returns | -|------|----------|---------|---------| -| (none yet) | - | - | - | - ---- - -## 🏪 Zustand Stores +/** + * Install .codex/skills//SKILL.md for every command in the catalog. + * - writes are idempotent (same input -> same bytes) + * - stale TOH-managed skills (generator marker, no longer in catalog) are removed + * - user skills (including user skills named toh-*) are never touched + * Returns the list of installed skill names. + */ +export async function installCodexSkills(targetDir, srcDir) { + const catalog = await readCommandCatalog(srcDir); + const skillsRoot = path.join(targetDir, '.codex', 'skills'); + await fs.ensureDir(skillsRoot); + + const wanted = new Set(catalog.map((c) => c.skillName)); + + // Remove stale TOH-managed skills (identified by the generator marker — + // never by directory name alone, so user skills are safe). + for (const entry of await fs.readdir(skillsRoot, { withFileTypes: true })) { + if (!entry.isDirectory() || wanted.has(entry.name)) continue; + const skillFile = path.join(skillsRoot, entry.name, 'SKILL.md'); + if (!(await fs.pathExists(skillFile))) continue; + try { + const head = (await fs.readFile(skillFile, 'utf8')).slice(0, 4096); + if (head.includes(`generator: ${SKILL_GENERATOR}`)) { + await fs.remove(path.join(skillsRoot, entry.name)); + } + } catch { + // Unreadable file -> leave it alone; never delete what we can't classify. + } + } -| Store | Location | State Shape | Key Actions | -|-------|----------|-------------|-------------| -| (none yet) | - | - | - | + for (const entry of catalog) { + const dir = path.join(skillsRoot, entry.skillName); + await fs.ensureDir(dir); + await fs.writeFile(path.join(dir, 'SKILL.md'), renderSkillMd(entry)); + } ---- + return catalog.map((c) => c.skillName); +} -## 🛠️ Utility Functions +// ============================================================ +// AGENTS.md (managed block only) +// ============================================================ + +function renderSkillTable(catalog) { + const rows = catalog + .map((c) => `| \`$${c.skillName}\` | ${c.description} |`) + .join('\n'); + return `| Skill | Use it to | +|-------|-----------| +${rows}`; +} -| Function | Location | Purpose | Params | -|----------|----------|---------|--------| -| cn | \`lib/utils.ts\` | Merge Tailwind classes | \`...inputs\` | +function generateAgentsMdEN(catalog) { + return `${AGENTS_BLOCK_START} +# 🎯 Toh Framework ---- +> **"Type Once, Have it all!"** — AI-Orchestration Driven Development -## 📊 Component Statistics +## Identity -| Category | Count | -|----------|-------| -| Pages | 1 | -| Components | 0 | -| Hooks | 0 | -| Stores | 0 | +You are the **Toh Framework Agent** running in **Codex CLI**, helping solo developers build SaaS systems by themselves. ---- -*Last updated: ${timestamp}* -`; +${renderCapabilitiesSection('codex')} - // changelog.md (v1.8.0 - Session Changelog) - const changelogContent = `# 📝 Session Changelog +## Using TOH in Codex (native skills) -## [Current Session] - ${timestamp} +TOH workflows are installed as **native Codex skills** under \`.codex/skills/\` — this is the canonical integration: -### Changes Made -| Agent | Action | File/Component | -|-------|--------|----------------| -| - | - | - | +${renderSkillTable(catalog)} -### Next Session TODO -- [ ] Continue from: [last task] +- Invoke explicitly with \`$\` (or browse with \`/skills\`), or describe the task — Codex matches skills by description. +- Each skill reads its workflow from \`.toh/commands/\` and supporting rules from \`.toh/skills/\`. ---- -*Auto-updated by agents after each task* -`; +### Legacy \`/toh-*\` compatibility text - // agents-log.md (v1.8.0 - Agent Activity Log) - const agentsLogContent = `# 🤖 Agents Activity Log +Codex has no custom slash commands. If the user types \`/toh-plan\` or \`toh plan\`, interpret it as a request to use the matching skill in \`.codex/skills/\` — compatibility behavior, not a native command. -## Recent Activity -| Time | Agent | Task | Status | Files | -|------|-------|------|--------|-------| -| - | - | - | - | - | +## TOH runtime & state (\`.toh/\`) -## Agent Statistics -- Total Tasks: 0 -- Success Rate: 100% +- \`.toh/plan.md\` + \`.toh/progress.md\` — the plan IS a file; resume = first unchecked \`[ ]\` task +- \`.toh/memory/\` — 7-file memory (protocol below) +- \`.toh/skills/\` · \`.toh/commands/\` · \`.toh/capabilities.json\` ---- -*Auto-updated by agents during execution* -`; +## Codex constraints (vs Claude Code) - // Write all 7 memory files (v1.8.0) - await fs.writeFile(path.join(memoryDir, 'active.md'), activeContent); - await fs.writeFile(path.join(memoryDir, 'summary.md'), summaryContent); - await fs.writeFile(path.join(memoryDir, 'decisions.md'), decisionsContent); - await fs.writeFile(path.join(memoryDir, 'architecture.md'), architectureContent); - await fs.writeFile(path.join(memoryDir, 'components.md'), componentsContent); - await fs.writeFile(path.join(memoryDir, 'changelog.md'), changelogContent); - await fs.writeFile(path.join(memoryDir, 'agents-log.md'), agentsLogContent); -} +- **No subagents/teams** — run THE TOH LOOP sequentially in this session (see \`.toh/skills/orchestration-protocol/SKILL.md\`): implement → run the task's checkpoint → quote real output → fix if red (max 5 tries; 3 consecutive failures = \`- [!] BLOCKED\`) → tick the checkbox → next task without asking. +- **No Stop hook** — self-enforce: never end the session while \`.toh/plan.md\` has unchecked, unblocked tasks. +- **No model routing** — ignore haiku/sonnet/opus tiers in TOH docs. -export async function setupCodex(targetDir, srcDir, language = 'en') { - // Create .toh/memory directory structure (v1.1.0 - Memory System) - const tohDir = path.join(targetDir, '.toh'); - const memoryDir = path.join(tohDir, 'memory'); - const archiveDir = path.join(memoryDir, 'archive'); - await fs.ensureDir(archiveDir); - - // Create memory template files - await createMemoryFiles(memoryDir, language); - - // Read all agents - const srcAgentsDir = path.join(srcDir, 'agents'); - let agentSections = ''; - - if (await fs.pathExists(srcAgentsDir)) { - const agentFiles = await fs.readdir(srcAgentsDir); - for (const file of agentFiles) { - if (file.endsWith('.md') && file !== 'README.md') { - const raw = await fs.readFile(path.join(srcAgentsDir, file), 'utf-8'); - const agentName = file.replace('.md', ''); - // v2.0: agents now carry SUPERSET frontmatter (name/description/tools/ - // model/skills/triggers). Strip the leading YAML block so Codex embeds a - // clean body only — no tools/model YAML leaking into AGENTS.md. - const body = raw.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, '').trimStart(); - agentSections += ` -### toh-${agentName} - -${body} +## Memory protocol (tiered) ---- -`; - } - } - } - - // Read commands summary - const srcCommandsDir = path.join(srcDir, 'commands'); - let commandsList = ''; - - if (await fs.pathExists(srcCommandsDir)) { - const commandFiles = await fs.readdir(srcCommandsDir); - for (const file of commandFiles) { - if (file.endsWith('.md') && file !== 'README.md') { - const cmdName = file.replace('.md', '').replace('toh-', '/toh-'); - commandsList += `- \`${cmdName}\`\n`; - } - } - } +- BEFORE work: read \`.toh/memory/active.md\` + \`summary.md\` (always); \`architecture.md\` + \`components.md\` for build tasks; \`changelog.md\` for debugging; \`decisions.md\` / \`agents-log.md\` only when referenced. +- AFTER work: always update \`active.md\`; update the others per relevance. Memory files are always in English. +- Close every stage per \`.toh/skills/engineer-harness/SKILL.md\` (Status / Result / Evidence / exactly 3 next actions). - // v2.0: run the assembled markdown through the shared marker transform so any - // blocks in embedded command/agent markdown are removed and - // blocks are unwrapped for Codex (idempotent, additive). - const agentsMd = transformCommand( - language === 'th' - ? generateAgentsMdTH(commandsList, agentSections) - : generateAgentsMdEN(commandsList, agentSections), - 'codex' - ); - - // Check if AGENTS.md exists - const agentsPath = path.join(targetDir, 'AGENTS.md'); - - if (await fs.pathExists(agentsPath)) { - // Read existing content - let existing = await fs.readFile(agentsPath, 'utf-8'); - - // Replace TOH section if exists, otherwise append - if (existing.includes('')) { - existing = existing.replace( - /[\s\S]*/, - agentsMd.trim() - ); - await fs.writeFile(agentsPath, existing); - } else { - await fs.appendFile(agentsPath, '\n\n' + agentsMd); - } - } else { - await fs.writeFile(agentsPath, agentsMd); - } - - return true; +${AGENTS_BLOCK_END}`; } -function generateAgentsMdEN(commandsList, agentSections) { - return ` +function generateAgentsMdTH(catalog) { + return `${AGENTS_BLOCK_START} # 🎯 Toh Framework -> **"Type Once, Have it all!"** - AI-Orchestration Driven Development - -## Project Memory - -This file serves as project memory for Codex CLI/Web. It contains the Toh Framework configuration and agent definitions. +> **"Type Once, Have it all!"** — AI-Orchestration Driven Development +> "สั่งครั้งเดียว จบครบโดยไม่ต้องถาม" ## Identity -You are the **Toh Framework Agent** - an AI that helps Solo Developers build SaaS systems by themselves. +คุณคือ **Toh Framework Agent** ที่รันอยู่บน **Codex CLI** — ช่วย Solo Developer สร้าง SaaS คนเดียวจนจบ ${renderCapabilitiesSection('codex')} -Runtime Identity: you are running in Codex CLI. Multi-agent features (subagents/teams) are unavailable here — execute the TOH LOOP sequentially in this session: implement -> run the story's checkpoint -> quote the actual output -> fix if red (max 5 tries, 3 consecutive failures = mark [!] BLOCKED and move on) -> tick the checkbox -> next story WITHOUT asking. Interrupted runs resume at the first unchecked box in .toh/plan.md. Close every stage with the engineer-harness announce contract (Status/Result/Evidence/exactly 3 next actions). - -## Core Philosophy (AODD - AI-Orchestration Driven Development) - -1. **Natural Language → Tasks** - Users give commands in plain language, you break them into tasks -2. **Orchestrator → Agents** - Automatically invoke relevant agents to complete work -3. **Users Don't Touch the Process** - No questions, no waiting, just deliver results -4. **Test → Fix → Loop** - Test, fix issues, repeat until passing - -## Tech Stack (Fixed - NEVER CHANGE) - -| Category | Technology | -|----------|------------| -| Framework | Next.js 14 (App Router) | -| Styling | Tailwind CSS + shadcn/ui | -| State | Zustand | -| Forms | React Hook Form + Zod | -| Backend | Supabase | -| Testing | Playwright | -| Language | TypeScript (strict) | - -## Language Rules - -- **Response Language:** Respond in the same language the user uses (if unclear, default to English) -- **UI Labels/Buttons:** English (Save, Cancel, Dashboard) -- **Mock Data:** English names, addresses, phone numbers -- **Code Comments:** English -- **Validation Messages:** English - -If user writes in Thai, respond in Thai. - -## 🚨 Command Recognition (CRITICAL) - -> **YOU MUST recognize and execute these commands immediately!** -> When user types ANY of these patterns, treat them as direct commands. - -### Command Patterns to Recognize: - -| Full Command | Shortcuts (ALL VALID) | Action | -|-------------|----------------------|--------| -| \`/toh-help\` | \`/toh-h\`, \`toh help\`, \`toh h\` | Show all commands | -| \`/toh-plan\` | \`/toh-p\`, \`toh plan\`, \`toh p\` | **THE BRAIN** - Analyze, plan | -| \`/toh-vibe\` | \`/toh-v\`, \`toh vibe\`, \`toh v\` | Create new project | -| \`/toh-ui\` | \`/toh-u\`, \`toh ui\`, \`toh u\` | Create UI components | -| \`/toh-dev\` | \`/toh-d\`, \`toh dev\`, \`toh d\` | Add logic & state | -| \`/toh-design\` | \`/toh-ds\`, \`toh design\`, \`toh ds\` | Improve design | -| \`/toh-test\` | \`/toh-t\`, \`toh test\`, \`toh t\` | Auto test & fix | -| \`/toh-connect\` | \`/toh-c\`, \`toh connect\`, \`toh c\` | Connect Supabase | -| \`/toh-line\` | \`/toh-l\`, \`toh line\`, \`toh l\` | LINE MINI App (convert) | -| \`/toh-mobile\` | \`/toh-m\`, \`toh mobile\`, \`toh m\` | PWA / Capacitor | -| \`/toh-fix\` | \`/toh-f\`, \`toh fix\`, \`toh f\` | Fix bugs | -| \`/toh-ship\` | \`/toh-s\`, \`toh ship\`, \`toh s\` | Deploy to production | -| \`/toh-protect\` | \`/toh-pr\`, \`toh protect\`, \`toh pr\` | Security audit | - -### ⚡ Execution Rules: - -1. **Instant Recognition** - When you see \`/toh-\` or \`toh \` prefix, this is a COMMAND -2. **Check for Description** - Does the command have a description after it? - - ✅ **Has description** → Execute immediately - - ❓ **No description** → Ask user first: "I'm the [Agent Name] agent. What would you like me to help you with?" -3. **No Confirmation for Described Commands** - If description exists, execute without asking -4. **Follow Memory Protocol** - Read/write \`.toh/memory/\` before/after - -### Command Without Description Behavior: - -| Command Only | Response | -|-------------|----------| -| \`/toh-vibe\` | "I'm the **Vibe Agent** 🎨. What system would you like me to build?" | -| \`/toh-ui\` | "I'm the **UI Agent** 🖼️. What UI would you like me to create?" | -| \`/toh-dev\` | "I'm the **Dev Agent** ⚙️. What functionality should I implement?" | -| \`/toh-design\` | "I'm the **Design Agent** ✨. What should I polish?" | -| \`/toh-test\` | "I'm the **Test Agent** 🧪. What should I test?" | -| \`/toh-connect\` | "I'm the **Connect Agent** 🔌. What should I connect?" | -| \`/toh-plan\` | "I'm the **Plan Agent** 🧠. What project should I plan?" | -| \`/toh-help\` | (Always show help immediately) | - -### Examples: - -\`\`\` -User: /toh-v restaurant management -→ Execute /toh-vibe to create restaurant management system - -User: toh ui dashboard -→ Execute /toh-ui to create dashboard UI -\`\`\` - -## Available Commands - -| Command | Description | -|---------|-------------| -| \`/toh-help\` | Show all available commands | -| \`/toh-plan\` | **THE BRAIN** — writes .toh/plan.md, one approval, then builds autonomously | -| \`/toh-vibe\` | Create new project with UI + Logic + Mock Data | -| \`/toh-ui\` | Create UI - Pages, Components, Layouts | -| \`/toh-dev\` | Add Logic - TypeScript, Zustand, Forms | -| \`/toh-design\` | Improve Design - Make it look professional | -| \`/toh-test\` | Test system - Auto test & fix until passing | -| \`/toh-connect\` | Connect Backend - Supabase, Auth, RLS | -| \`/toh-line\` | LINE MINI App - convert (LIFF SDK) | -| \`/toh-mobile\` | Mobile App - PWA / Capacitor | -| \`/toh-fix\` | Fix bugs - Debug and fix issues | -| \`/toh-ship\` | Deploy - Vercel, Production ready | -| \`/toh-protect\` | Security audit - Full security check | - -## Memory System (Auto, 7 files — Tiered Loading) - -Toh Framework has automatic memory at \`.toh/memory/\`. Read only what the task needs: -- **Tier 1 (ALWAYS read, ~800 tokens):** \`active.md\` (current task) + \`summary.md\` (project overview) -- **Tier 2 (per task type):** \`architecture.md\` + \`components.md\` for build/code work; \`changelog.md\` for debug work -- **Tier 3 (only when referenced):** \`decisions.md\` (past decisions) + \`agents-log.md\` (agent activity) -- \`archive/\` - Historical data (on-demand only) - -## 🚨 MANDATORY: Memory Protocol (Tiered Loading) - -> **CRITICAL:** You MUST follow this protocol EVERY time! Never read all 7 files by reflex. - -### BEFORE Starting ANY Work: -1. Check \`.toh/memory/\` folder exists -2. Read Tier 1: \`.toh/memory/active.md\` + \`.toh/memory/summary.md\` -3. Read Tier 2 for this task type (build/code → \`architecture.md\` + \`components.md\`; debug → \`changelog.md\`) -4. Read Tier 3 (\`decisions.md\`, \`agents-log.md\`) ONLY when referenced -5. If files empty but project has code → ANALYZE and populate first! -6. Acknowledge: "Memory loaded! [Brief context]" - -### AFTER Completing ANY Work (write per relevance): -1. Update \`.toh/memory/active.md\` - ALWAYS (what was done, next steps) -2. Update \`.toh/memory/summary.md\` - when the project shape changes (feature done / new structure) -3. Update \`.toh/memory/architecture.md\` / \`components.md\` - when modules/stores/hooks/utils change -4. Update \`.toh/memory/changelog.md\` + \`agents-log.md\` - record the change and which agent did it -5. Update \`.toh/memory/decisions.md\` - if a real decision was made -6. Confirm: "Memory saved ✅" - -### ⚠️ CRITICAL RULES: -- NEVER start work without reading Tier 1 (active.md + summary.md)! -- NEVER finish work without updating active.md! -- Read Tier 2 / Tier 3 only when the task type or a reference calls for it! -- Memory files must ALWAYS be in English! - -## Command Usage Examples - -### Create New Project -\`\`\` -/toh-vibe A coffee shop management system with POS, inventory, and sales reports -\`\`\` - -### Add UI -\`\`\` -/toh-ui Add a dashboard page showing daily sales -\`\`\` - -### Add Logic -\`\`\` -/toh-dev Make the date filter work properly -\`\`\` - -### Improve Design -\`\`\` -/toh-design Make it look professional, not like AI-generated -\`\`\` - -### Test System -\`\`\` -/toh-test Test all pages -\`\`\` - -### Connect Backend -\`\`\` -/toh-connect Connect to Supabase with auth -\`\`\` - -### Deploy -\`\`\` -/toh-ship Deploy to Vercel -\`\`\` - -## Behavior Rules - -1. **Don't ask basic questions** - Make decisions yourself -2. **Use the fixed tech stack** - Never change it -3. **Respond in English** - All communication in English -4. **English Mock Data** - Use English names, addresses, phone numbers -5. **UI First** - Create working UI before backend -6. **Production Ready** - Not a prototype - -## Mock Data Examples - -Use realistic English data: -- Names: John, Mary, Michael, Sarah -- Last names: Smith, Johnson, Williams -- Cities: New York, Los Angeles, Chicago -- Phone: (555) 123-4567 -- Email: john.smith@example.com - -## Agents - -${agentSections} - -## 🚨 MANDATORY: Skills & Agents Loading - -> **CRITICAL:** Before executing ANY /toh- command, you MUST load the required skills! - -### Command → Skills Map - -| Command | Load These Skills (from \`.toh/skills/\`) | -|---------|------------------------------------------| -| \`/toh-vibe\` | \`vibe-orchestrator\`, \`orchestration-protocol\`, \`premium-experience\`, \`design-craft\`, \`ui-first-builder\`, \`engineer-harness\` | -| \`/toh-ui\` | \`ui-first-builder\`, \`design-craft\`, \`engineer-harness\` | -| \`/toh-dev\` | \`dev-engineer\`, \`backend-engineer\`, \`engineer-harness\` | -| \`/toh-design\` | \`design-craft\`, \`premium-experience\` | -| \`/toh-test\` | \`test-engineer\`, \`debug-protocol\`, \`error-handling\` | -| \`/toh-connect\` | \`backend-engineer\`, \`integrations\` | -| \`/toh-plan\` | \`plan-orchestrator\`, \`orchestration-protocol\`, \`business-context\`, \`smart-routing\`, \`engineer-harness\` | -| \`/toh-fix\` | \`debug-protocol\`, \`error-handling\`, \`test-engineer\` | -| \`/toh-line\` | \`platform-specialist\`, \`integrations\` | -| \`/toh-mobile\` | \`platform-specialist\`, \`ui-first-builder\` | -| \`/toh-ship\` | \`version-control\`, \`progress-tracking\` | - -### Core Skills (Always Available) -- \`memory-system\` - Memory read/write protocol -- \`engineer-harness\` - Smart tool selection + human-friendly reporting + next steps -- \`smart-routing\` - Command routing logic - -### Loading Protocol: -1. User types /toh-[command] -2. Read required skill files from \`.toh/skills/[skill-name]/SKILL.md\` -3. Execute following skill instructions -4. Save memory after completion +## การใช้ TOH ใน Codex (native skills) -### ⚠️ NEVER Skip Skills! -Skills contain CRITICAL best practices, design tokens, and rules. +เวิร์กโฟลว์ TOH ถูกติดตั้งเป็น **native Codex skills** ไว้ที่ \`.codex/skills/\` — นี่คือช่องทางหลัก: -## 🔒 Skills Loading Checkpoint (REQUIRED) +${renderSkillTable(catalog)} -> **ENFORCEMENT:** You MUST report skills loaded at the START of your response! +- เรียกตรงๆ ด้วย \`$\` (หรือพิมพ์ \`/skills\` เพื่อดูทั้งหมด) หรือแค่บรรยายงาน — Codex จะจับคู่ skill จาก description เอง +- แต่ละ skill อ่านเวิร์กโฟลว์จาก \`.toh/commands/\` และกฎประกอบจาก \`.toh/skills/\` -### Required Response Start: +### ข้อความ \`/toh-*\` แบบเดิม (compatibility) -\`\`\`markdown -📚 **Skills Loaded:** -- skill-name-1 ✅ (brief what you learned) -- skill-name-2 ✅ (brief what you learned) +Codex ไม่มี slash command แบบกำหนดเอง ถ้าผู้ใช้พิมพ์ \`/toh-plan\` หรือ \`toh plan\` ให้ตีความว่าเป็นคำขอใช้ skill ที่ตรงกันใน \`.codex/skills/\` — เป็น compatibility behavior ไม่ใช่ native command -🤖 **Agent:** agent-name +## TOH runtime & state (\`.toh/\`) -💾 **Memory:** Loaded ✅ +- \`.toh/plan.md\` + \`.toh/progress.md\` — แผนคือไฟล์; resume = task แรกที่ยังไม่ติ๊ก \`[ ]\` +- \`.toh/memory/\` — memory 7 ไฟล์ (protocol ด้านล่าง) +- \`.toh/skills/\` · \`.toh/commands/\` · \`.toh/capabilities.json\` ---- +## ข้อจำกัดของ Codex (เทียบ Claude Code) -[Then continue with your work...] -\`\`\` - -### Why This Matters: -- If you don't report skills → You didn't read them -- If you skip skills → Output quality drops significantly -- Skills have design tokens, patterns, and critical rules -- This checkpoint proves you followed the protocol - -## Skills Reference - -All skills are in \`.toh/skills/\` (Central Resources): -- \`vibe-orchestrator\` - Core methodology -- \`ui-first-builder\` - UI patterns -- \`dev-engineer\` - TypeScript, State, Forms -- \`design-craft\` - Design system, anti-patterns & business-appropriate fit -- \`premium-experience\` - Premium multi-page apps -- \`test-engineer\` - Testing with Playwright -- \`backend-engineer\` - Supabase integration -- \`platform-specialist\` - LINE, Mobile, Desktop -- \`memory-system\` - Memory protocol -- \`engineer-harness\` - Smart tool selection, reporting & next steps -- \`debug-protocol\` - Debugging guide -- \`error-handling\` - Error handling patterns - -## Getting Started - -Start with: -\`\`\` -/toh-vibe [describe what system you want] -\`\`\` - -The AI will: -1. Analyze your requirements -2. Break down into tasks -3. Create UI with English mock data -4. Add logic and state management -5. Polish the design -6. Deliver production-ready code +- **ไม่มี subagents/teams** — รัน THE TOH LOOP แบบ sequential ใน session นี้ (ดู \`.toh/skills/orchestration-protocol/SKILL.md\`): implement → รัน checkpoint ของ task → quote output จริง → แก้ถ้าแดง (สูงสุด 5 ครั้ง; แพ้ 3 ครั้งติด = \`- [!] BLOCKED\`) → ติ๊ก checkbox → ทำ task ถัดไปโดยไม่ต้องถาม +- **ไม่มี Stop hook** — บังคับตัวเอง: ห้ามจบ session ถ้า \`.toh/plan.md\` ยังมี task ที่ไม่ได้ติ๊กและไม่ blocked +- **ไม่มี model routing** — ข้าม tier haiku/sonnet/opus ในเอกสาร TOH ---- +## Memory protocol (tiered) -**GitHub:** https://github.com/ArtificialWeb/toh-framework -**Author:** Wasin Treesinthuros (Innovation Vantage) +- ก่อนทำงาน: อ่าน \`.toh/memory/active.md\` + \`summary.md\` เสมอ; \`architecture.md\` + \`components.md\` สำหรับงาน build; \`changelog.md\` สำหรับงาน debug; \`decisions.md\` / \`agents-log.md\` เฉพาะเมื่อถูกอ้างถึง +- หลังทำงาน: อัปเดต \`active.md\` เสมอ; ไฟล์อื่นตามความเกี่ยวข้อง — memory ทุกไฟล์เป็นภาษาอังกฤษ +- ปิดทุก stage ตาม \`.toh/skills/engineer-harness/SKILL.md\` (Status / Result / Evidence / next actions 3 ข้อ) - -`; +${AGENTS_BLOCK_END}`; } -function generateAgentsMdTH(commandsList, agentSections) { - return ` -# 🎯 Toh Framework - -> **"Type Once, Have it all!"** - AI-Orchestration Driven Development -> **"Command once, done without questions"** - -## Project Memory - -This file is project memory for Codex CLI/Web containing Toh Framework configuration and agent definitions - -## Identity - -You are **Toh Framework Agent** - AI that helps Solo Developers build SaaS by themselves - -${renderCapabilitiesSection('codex')} - -Runtime Identity: you are running in Codex CLI. Multi-agent features (subagents/teams) are unavailable here — execute the TOH LOOP sequentially in this session: implement -> run the story's checkpoint -> quote the actual output -> fix if red (max 5 tries, 3 consecutive failures = mark [!] BLOCKED and move on) -> tick the checkbox -> next story WITHOUT asking. Interrupted runs resume at the first unchecked box in .toh/plan.md. Close every stage with the engineer-harness announce contract (Status/Result/Evidence/exactly 3 next actions). - -## Core Philosophy (AODD - AI-Orchestration Driven Development) - -1. **Human Language → Tasks** - User commands naturally, you break into tasks -2. **Orchestrator → Agents** - Call relevant agents to work automatically -3. **User doesn't handle process** - No questions, no waiting, just complete it -4. **Test → Fix → Loop** - Test, fix, until pass - -## Tech Stack (Do not change!) - -| Category | Technology | -|------|----------| -| Framework | Next.js 14 (App Router) | -| Styling | Tailwind CSS + shadcn/ui | -| State | Zustand | -| Forms | React Hook Form + Zod | -| Backend | Supabase | -| Testing | Playwright | -| Language | TypeScript (strict) | - -## Language Rules - -- **Response Language:** Match user's language (if unsure, use Thai) -- **UI Labels/Buttons:** Thai (Save, Cancel, Dashboard) -- **Mock Data:** Thai names, addresses, phone numbers -- **Code Comments:** Thai allowed -- **Validation Messages:** Thai - -If user types in English, respond in English - -## 🚨 Command Handling (Very Important!) - -> **You must remember and execute these commands immediately!** -> When user types any pattern below, treat it as a direct command - -### Command Patterns to Remember: - -| Full Command | Shortcuts (ALL VALID) | Action | -|-------------|----------------------|--------| -| \`/toh-help\` | \`/toh-h\`, \`toh help\`, \`toh h\` | Show all commands | -| \`/toh-plan\` | \`/toh-p\`, \`toh plan\`, \`toh p\` | 🧠 THE BRAIN - Analyze, plan | -| \`/toh-vibe\` | \`/toh-v\`, \`toh vibe\`, \`toh v\` | Create new project | -| \`/toh-ui\` | \`/toh-u\`, \`toh ui\`, \`toh u\` | Create UI | -| \`/toh-dev\` | \`/toh-d\`, \`toh dev\`, \`toh d\` | Add logic & state | -| \`/toh-design\` | \`/toh-ds\`, \`toh design\`, \`toh ds\` | Improve design | -| \`/toh-test\` | \`/toh-t\`, \`toh test\`, \`toh t\` | Auto test & fix | -| \`/toh-connect\` | \`/toh-c\`, \`toh connect\`, \`toh c\` | Connect Supabase | -| \`/toh-line\` | \`/toh-l\`, \`toh line\`, \`toh l\` | LINE MINI App (convert) | -| \`/toh-mobile\` | \`/toh-m\`, \`toh mobile\`, \`toh m\` | Mobile App (PWA / Capacitor) | -| \`/toh-fix\` | \`/toh-f\`, \`toh fix\`, \`toh f\` | Fix bugs | -| \`/toh-ship\` | \`/toh-s\`, \`toh ship\`, \`toh s\` | Deploy to production | -| \`/toh-protect\` | \`/toh-pr\`, \`toh protect\`, \`toh pr\` | Security audit | - -### ⚡ Execution Rules: - -1. **Remember Immediately** - See \`/toh-\` or \`toh \` = command! -2. **Check Description** - Does command have description after? - - ✅ **Has description** → Execute immediately - - ❓ **No description** → Ask first: "I'm [Agent Name], what would you like me to help with?" -3. **No confirmation if Description exists** - Has description = execute -4. **Follow Memory Protocol** - Read/write \`.toh/memory/\` - -### Behavior When No Description: - -| Command Only | Response | -|-----------|--------| -| \`/toh-vibe\` | "I'm **Vibe Agent** 🎨, what system would you like me to create?" | -| \`/toh-ui\` | "I'm **UI Agent** 🖼️, what UI would you like me to create?" | -| \`/toh-dev\` | "I'm **Dev Agent** ⚙️, what functionality would you like me to add?" | -| \`/toh-design\` | "I'm **Design Agent** ✨, what would you like me to improve?" | -| \`/toh-test\` | "I'm **Test Agent** 🧪, what would you like me to test?" | -| \`/toh-connect\` | "I'm **Connect Agent** 🔌, what would you like me to connect?" | -| \`/toh-plan\` | "I'm **Plan Agent** 🧠, what would you like me to plan?" | -| \`/toh-help\` | (Always show help immediately) | - -### Examples: - -\`\`\` -User: /toh-v restaurant management system -→ Execute /toh-vibe create restaurant management system - -User: toh ui dashboard -→ Execute /toh-ui create dashboard -\`\`\` - -## Available Commands - -| Command | Description | -|---------|-------------| -| \`/toh-help\` | Show all commands | -| \`/toh-plan\` | 🧠 **THE BRAIN** — writes .toh/plan.md, one approval, then builds autonomously | -| \`/toh-vibe\` | Create new project - UI + Logic + Mock Data | -| \`/toh-ui\` | Create UI - Pages, Components, Layouts | -| \`/toh-dev\` | Add Logic - TypeScript, Zustand, Forms | -| \`/toh-design\` | Polish Design - Make it beautiful, not AI-looking | -| \`/toh-test\` | Test system - Auto test & fix until pass | -| \`/toh-connect\` | Connect Backend - Supabase, Auth, RLS | -| \`/toh-line\` | LINE MINI App - convert (LIFF SDK) | -| \`/toh-mobile\` | Mobile App - PWA / Capacitor | -| \`/toh-fix\` | Fix Bug - Debug and fix issues | -| \`/toh-ship\` | Deploy - Vercel, Production ready | -| \`/toh-protect\` | 🔐 Security Audit - Full security check | - -## Memory System (Automatic, 7 files — Tiered Loading) - -Toh Framework has Memory system at \`.toh/memory/\`. Read only what the task needs: -- **Tier 1 (ALWAYS read, ~800 tokens):** \`active.md\` (current task) + \`summary.md\` (project overview) -- **Tier 2 (per task type):** \`architecture.md\` + \`components.md\` for build/code work; \`changelog.md\` for debug work -- **Tier 3 (only when referenced):** \`decisions.md\` (past decisions) + \`agents-log.md\` (agent activity) -- \`archive/\` - Historical data (load when needed) - -## 🚨 Required: Memory Protocol (Tiered Loading) - -> **Important:** Must follow this every time! Never read all 7 files by reflex. - -### Before Starting Work: -1. Check if \`.toh/memory/\` folder exists -2. Read Tier 1: \`.toh/memory/active.md\` + \`.toh/memory/summary.md\` -3. Read Tier 2 for this task type (build/code → \`architecture.md\` + \`components.md\`; debug → \`changelog.md\`) -4. Read Tier 3 (\`decisions.md\`, \`agents-log.md\`) ONLY when referenced -5. If files empty but code exists → Analyze project first! -6. Tell User: "Memory loaded! [brief summary]" - -### After Completing Work (write per relevance): -1. Update \`.toh/memory/active.md\` - ALWAYS (What was done, next steps) -2. Update \`.toh/memory/summary.md\` - when the project shape changes (feature done / new structure) -3. Update \`.toh/memory/architecture.md\` / \`components.md\` - when modules/stores/hooks/utils change -4. Update \`.toh/memory/changelog.md\` + \`agents-log.md\` - record the change and which agent did it -5. Update \`.toh/memory/decisions.md\` - if a real decision was made -6. Tell User: "Memory saved ✅" - -### ⚠️ Important Rules: -- Never start work without reading Tier 1 (active.md + summary.md)! -- Never finish work without updating active.md! -- Read Tier 2 / Tier 3 only when the task type or a reference calls for it! -- Memory files must always be in English! - -## Usage Examples - -### Create New Project -\`\`\` -/toh-vibe coffee shop management with POS, inventory, sales reports -\`\`\` - -### Add UI -\`\`\` -/toh-ui add dashboard page showing daily sales -\`\`\` - -### Add Logic -\`\`\` -/toh-dev make date filter actually work -\`\`\` - -### Polish Design -\`\`\` -/toh-design make it look professional, not AI-generated -\`\`\` - -### Test System -\`\`\` -/toh-test test all pages -\`\`\` - -### Connect Backend -\`\`\` -/toh-connect connect Supabase with auth -\`\`\` - -### Deploy -\`\`\` -/toh-ship deploy to Vercel -\`\`\` - -## Rules to Follow - -1. **No Basic Questions** - Decide yourself -2. **Use Fixed Tech Stack** - Don't change -3. **Respond in Thai** - All communication in Thai -4. **Thai Mock Data** - Use Thai names, addresses, phone numbers -5. **UI First** - Build UI first to visualize -6. **Production Ready** - Not a prototype - -## Mock Data Examples - -Use realistic Thai data: -- First names: Somchai, Somying, Manee, Mana -- Last names: Jaidee, Rakrian, Suksun -- Addresses: Bangkok, Chiang Mai, Phuket -- Phone: 081-234-5678 -- Email: somchai@example.com - -## Agents - -${agentSections} - -## 🚨 Required: Load Skills & Agents - -> **Important:** Before executing any /toh- command, must load related skills! - -### Command → Skills Map - -| Command | Load These Skills (from \`.toh/skills/\`) | -|--------|-------------------------------------------| -| \`/toh-vibe\` | \`vibe-orchestrator\`, \`orchestration-protocol\`, \`premium-experience\`, \`design-craft\`, \`ui-first-builder\`, \`engineer-harness\` | -| \`/toh-ui\` | \`ui-first-builder\`, \`design-craft\`, \`engineer-harness\` | -| \`/toh-dev\` | \`dev-engineer\`, \`backend-engineer\`, \`engineer-harness\` | -| \`/toh-design\` | \`design-craft\`, \`premium-experience\` | -| \`/toh-test\` | \`test-engineer\`, \`debug-protocol\`, \`error-handling\` | -| \`/toh-connect\` | \`backend-engineer\`, \`integrations\` | -| \`/toh-plan\` | \`plan-orchestrator\`, \`orchestration-protocol\`, \`business-context\`, \`smart-routing\`, \`engineer-harness\` | -| \`/toh-fix\` | \`debug-protocol\`, \`error-handling\`, \`test-engineer\` | -| \`/toh-line\` | \`platform-specialist\`, \`integrations\` | -| \`/toh-mobile\` | \`platform-specialist\`, \`ui-first-builder\` | -| \`/toh-ship\` | \`version-control\`, \`progress-tracking\` | - -### Core Skills (Always Available) -- \`memory-system\` - Memory system -- \`engineer-harness\` - Smart tool selection + human-friendly reporting + next steps -- \`smart-routing\` - Command routing - -### Loading Steps: -1. User types /toh-[command] -2. Read skill files from \`.toh/skills/[skill-name]/SKILL.md\` -3. Execute according to skill instructions -4. Save memory after completion - -### ⚠️ Never Skip Skills! -Skills contain best practices, design tokens, and important rules - -## 🔒 Skills Loading Checkpoint (Required) +/** + * Insert or replace the TOH-managed block in AGENTS.md. All existing user + * content outside the markers is preserved; duplicate TOH blocks collapse + * into one, so repeated installs are idempotent by construction. + */ +export async function updateAgentsMd(targetDir, catalog, language = 'en') { + const block = language === 'th' ? generateAgentsMdTH(catalog) : generateAgentsMdEN(catalog); + const agentsPath = path.join(targetDir, 'AGENTS.md'); -> **Required:** Must report loaded skills at the beginning of response! + if (await fs.pathExists(agentsPath)) { + const existing = await fs.readFile(agentsPath, 'utf8'); + const stripped = existing.replace(AGENTS_BLOCK_RE, '').trimEnd(); + const next = stripped ? `${stripped}\n\n${block}\n` : `${block}\n`; + await fs.writeFile(agentsPath, next); + } else { + await fs.writeFile(agentsPath, `${block}\n`); + } + return agentsPath; +} -### Response Start Format: +// ============================================================ +// .toh runtime (seed-if-absent — install.js is the primary seeder) +// ============================================================ -\`\`\`markdown -📚 **Skills Loaded:** -- skill-name-1 ✅ (brief summary of what was loaded) -- skill-name-2 ✅ (brief summary of what was loaded) +/** + * Guarantee the TOH runtime skeleton exists. Seeds only when absent so a + * reinstall never clobbers live memory/plan state. install.js already creates + * the full runtime before IDE handlers run; this makes setupCodex() safe to + * call standalone (tests, partial reinstalls). + */ +export async function ensureTohRuntime(targetDir) { + const tohDir = path.join(targetDir, '.toh'); + const memoryDir = path.join(tohDir, 'memory'); + await fs.ensureDir(path.join(memoryDir, 'archive')); + + const today = new Date().toISOString().split('T')[0]; + const seeds = { + 'active.md': `# 🔥 Active Task\n\n## Current Work\n[No active task - Waiting for user command]\n\n## Last Action\n[None]\n\n## Next Steps\n- Waiting for user command\n\n## Blockers\n[None]\n`, + 'summary.md': `# 📋 Project Summary\n\n## Project Info\n- **Name:** [Not specified]\n- **Type:** [Not specified]\n\n## Completed Features\n[None yet]\n\n## In Progress\n[None yet]\n`, + 'decisions.md': `# 🧠 Key Decisions\n\n## Architecture Decisions\n| Date | Decision | Reason |\n|------|----------|--------|\n| ${today} | Use Toh Framework v${VERSION} | AI-Orchestration Driven Development |\n`, + 'changelog.md': `# 📝 Session Changelog\n\n## [Current Session] - ${today}\n\n### Changes Made\n| Agent | Action | File/Component |\n|-------|--------|----------------|\n| - | - | - |\n`, + 'agents-log.md': `# 🤖 Agents Activity Log\n\n## Recent Activity\n| Time | Agent | Task | Status | Files |\n|------|-------|------|--------|-------|\n| - | - | - | - | - |\n`, + 'architecture.md': `# 🏗️ Code Architecture\n\n## Directory Structure\n\`\`\`\n[Will be auto-generated when project starts]\n\`\`\`\n`, + 'components.md': `# 🧩 Component Registry\n\n## UI Components\n| Component | Path | Props | Used In |\n|-----------|------|-------|---------|\n| - | - | - | - |\n` + }; + + for (const [file, content] of Object.entries(seeds)) { + const p = path.join(memoryDir, file); + if (!(await fs.pathExists(p))) await fs.writeFile(p, content); + } -🤖 **Agent:** agent name + const planPath = path.join(tohDir, 'plan.md'); + if (!(await fs.pathExists(planPath))) { + await fs.writeFile( + planPath, + `# Plan: (no active plan yet)\nStatus: draft\nCreated: ${today} by toh-framework installer\n\n> This file is THE TOH LOOP's backlog. Full schema + loop protocol:\n> \`.toh/skills/orchestration-protocol/SKILL.md\` (Section D).\n\nEmpty backlog — no stories yet. Run the \`toh-plan\` skill to draft a plan here, or\n\`toh-vibe\` to auto-generate a mini-plan and build it.\n` + ); + } -💾 **Memory:** loaded ✅ + const progressPath = path.join(tohDir, 'progress.md'); + if (!(await fs.pathExists(progressPath))) { + await fs.writeFile( + progressPath, + `# Progress Ledger\n\n> Append-only, one line per state change: \`queued → running → done/failed/blocked\`.\n> Format: \`YYYY-MM-DD HH:MM T00x — \`. Never rewrite history — append.\n` + ); + } +} ---- +// ============================================================ +// Public API +// ============================================================ -[then continue with work...] -\`\`\` - -### Why This Is Required: -- If skills not reported → means not read -- If skills skipped → work quality will decrease significantly -- Skills contain design tokens, patterns, and important rules -- This checkpoint proves protocol compliance - -## Skills Reference - -All skills are located at \`.toh/skills/\` (Central Resources): -- \`vibe-orchestrator\` - Core methodology -- \`ui-first-builder\` - UI patterns -- \`dev-engineer\` - TypeScript, State, Forms -- \`design-craft\` - Design system, anti-patterns & business-appropriate fit -- \`premium-experience\` - Premium multi-page apps -- \`test-engineer\` - Testing with Playwright -- \`backend-engineer\` - Supabase integration -- \`platform-specialist\` - LINE, Mobile, Desktop -- \`memory-system\` - Memory protocol -- \`engineer-harness\` - Smart tool selection, reporting & next steps - -## Getting Started - -Start with: -\`\`\` -/toh-vibe [describe the system you want] -\`\`\` - -AI will: -1. Analyze requirements -2. Break down tasks -3. Create UI with Thai mock data -4. Add logic and state management -5. Polish design to look beautiful -6. Deliver production-ready code +/** + * Install the Codex integration. Returns a short detail string for the + * install spinner. + */ +export async function setupCodex(targetDir, srcDir, language = 'en') { + await ensureTohRuntime(targetDir); + const catalog = await readCommandCatalog(srcDir); + const installed = await installCodexSkills(targetDir, srcDir); + await updateAgentsMd(targetDir, catalog, language); + return `.codex/skills/ (${installed.length} skills) + AGENTS.md`; +} ---- +/** + * Remove ONLY the TOH-managed Codex files: + * - .codex/skills/ dirs whose SKILL.md carries the TOH generator marker + * - the TOH-managed block in AGENTS.md (file deleted if it becomes empty) + * User skills, user AGENTS.md content, and .toh/ runtime state are preserved. + * Returns { removedSkills, agentsMd: 'updated'|'removed'|'absent' }. + */ +export async function uninstallCodex(targetDir) { + const result = { removedSkills: [], agentsMd: 'absent' }; + + const skillsRoot = path.join(targetDir, '.codex', 'skills'); + if (await fs.pathExists(skillsRoot)) { + for (const entry of await fs.readdir(skillsRoot, { withFileTypes: true })) { + if (!entry.isDirectory()) continue; + const dir = path.join(skillsRoot, entry.name); + const skillFile = path.join(dir, 'SKILL.md'); + if (!(await fs.pathExists(skillFile))) continue; + try { + const head = (await fs.readFile(skillFile, 'utf8')).slice(0, 4096); + if (head.includes(`generator: ${SKILL_GENERATOR}`)) { + await fs.remove(dir); + result.removedSkills.push(entry.name); + } + } catch { + // Leave unreadable entries untouched. + } + } + result.removedSkills.sort(); + } -**GitHub:** https://github.com/ArtificialWeb/toh-framework -**Author:** Wasin Treesinthuros (Innovation Vantage) + const agentsPath = path.join(targetDir, 'AGENTS.md'); + if (await fs.pathExists(agentsPath)) { + const existing = await fs.readFile(agentsPath, 'utf8'); + const stripped = existing.replace(AGENTS_BLOCK_RE, '').trim(); + if (stripped === existing.trim()) { + result.agentsMd = 'absent'; // no TOH block -> nothing to do + } else if (stripped) { + await fs.writeFile(agentsPath, `${stripped}\n`); + result.agentsMd = 'updated'; + } else { + await fs.remove(agentsPath); // block was the whole file -> file was ours + result.agentsMd = 'removed'; + } + } - -`; + return result; } diff --git a/installer/install.js b/installer/install.js index 4db7668..88cf35e 100644 --- a/installer/install.js +++ b/installer/install.js @@ -12,13 +12,19 @@ import { dirname, join } from 'path'; import { setupClaudeCode } from './ide-handlers/claude-code.js'; import { setupCursor } from './ide-handlers/cursor.js'; import { setupGeminiCLI } from './ide-handlers/gemini-cli.js'; -import { setupCodex } from './ide-handlers/codex.js'; +import { setupCodex, uninstallCodex } from './ide-handlers/codex.js'; import { transformCommand, writeCapabilitiesJson } from './ide-handlers/shared.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const SRC_DIR = join(__dirname, '..', 'src'); +// ora corrupts the node:test child-process IPC channel (Node 24), so tests set +// TOH_QUIET=1 to silence spinners. Default CLI behavior is unchanged. +function startSpinner(text) { + return ora({ text, isSilent: process.env.TOH_QUIET === '1' }).start(); +} + // Read version from package.json (Single Source of Truth) const PKG_PATH = join(__dirname, '..', 'package.json'); const pkg = await fs.readJson(PKG_PATH); @@ -45,17 +51,21 @@ export async function install(options) { } // Validate target directory - const spinner = ora('Validating target directory...').start(); + const spinner = startSpinner('Validating target directory...'); if (!fs.existsSync(config.targetDir)) { - spinner.warn('Target directory does not exist'); - const { create } = await inquirer.prompt([{ - type: 'confirm', - name: 'create', - message: `Create directory ${config.targetDir}?`, - default: true - }]); - + // --quick is the non-interactive path: create the directory and proceed. + let create = quick; + if (!quick) { + spinner.warn('Target directory does not exist'); + ({ create } = await inquirer.prompt([{ + type: 'confirm', + name: 'create', + message: `Create directory ${config.targetDir}?`, + default: true + }])); + } + if (create) { fs.mkdirSync(config.targetDir, { recursive: true }); spinner.succeed('Directory created'); @@ -70,22 +80,26 @@ export async function install(options) { // Check for existing installation const existingInstall = await checkExistingInstall(config.targetDir); if (existingInstall) { - const { action } = await inquirer.prompt([{ - type: 'list', - name: 'action', - message: 'Existing Toh Framework installation detected. What would you like to do?', - choices: [ - { name: '🔄 Quick Update (preserve customizations)', value: 'update' }, - { name: '🗑️ Fresh Install (overwrite all)', value: 'fresh' }, - { name: '❌ Cancel', value: 'cancel' } - ] - }]); + // --quick must stay non-interactive: a reinstall defaults to Quick Update. + let action = 'update'; + if (!quick) { + ({ action } = await inquirer.prompt([{ + type: 'list', + name: 'action', + message: 'Existing Toh Framework installation detected. What would you like to do?', + choices: [ + { name: '🔄 Quick Update (preserve customizations)', value: 'update' }, + { name: '🗑️ Fresh Install (overwrite all)', value: 'fresh' }, + { name: '❌ Cancel', value: 'cancel' } + ] + }])); + } if (action === 'cancel') { console.log(chalk.yellow('\nInstallation cancelled.')); return; } - + if (action === 'fresh') { await cleanExistingInstall(config.targetDir); } @@ -218,8 +232,8 @@ async function checkExistingInstall(targetDir) { } async function cleanExistingInstall(targetDir) { - const spinner = ora('Cleaning existing installation...').start(); - + const spinner = startSpinner('Cleaning existing installation...'); + const pathsToClean = [ join(targetDir, '.toh'), join(targetDir, '.claude', 'skills'), @@ -232,12 +246,16 @@ async function cleanExistingInstall(targetDir) { await fs.remove(p); } } - + + // Codex: remove TOH-managed .codex/skills + the AGENTS.md TOH block, + // preserving any user skills and user AGENTS.md content. + await uninstallCodex(targetDir); + spinner.succeed('Cleaned existing installation'); } async function setupIDEWithSpinner(ideName, setupFn) { - const spinner = ora(`Configuring ${ideName}...`).start(); + const spinner = startSpinner(`Configuring ${ideName}...`); try { const detail = await setupFn(); const configFile = (typeof detail === 'string' && detail) ? detail : getIDEConfigFile(ideName); @@ -252,13 +270,13 @@ function getIDEConfigFile(ideName) { 'Claude Code': 'created CLAUDE.md', 'Cursor': '.cursor/rules/*.mdc', 'Gemini CLI': '.gemini/GEMINI.md', - 'Codex CLI': 'AGENTS.md' + 'Codex CLI': '.codex/skills/ + AGENTS.md' }; return configs[ideName] || 'configured'; } async function installComponent(componentName, targetDir) { - const spinner = ora(`Installing ${componentName}...`).start(); + const spinner = startSpinner(`Installing ${componentName}...`); const srcPath = join(SRC_DIR, componentName); let destPath; @@ -308,7 +326,7 @@ async function countFiles(dir) { } async function generateManifest(config) { - const spinner = ora('Generating manifest...').start(); + const spinner = startSpinner('Generating manifest...'); const manifest = { version: VERSION, @@ -340,7 +358,7 @@ async function normalizeUniversalCommands(targetDir) { const commandsDir = join(targetDir, '.toh', 'commands'); if (!fs.existsSync(commandsDir)) return; - const spinner = ora('Normalizing shared commands (universal variant)...').start(); + const spinner = startSpinner('Normalizing shared commands (universal variant)...'); try { let changed = 0; const walk = async (dir) => { @@ -371,7 +389,7 @@ async function normalizeUniversalCommands(targetDir) { * orchestration-protocol 2-step survey (identity lives in each context file). */ async function declareCapabilities(config) { - const spinner = ora('Declaring runtime capabilities...').start(); + const spinner = startSpinner('Declaring runtime capabilities...'); try { await writeCapabilitiesJson(config.targetDir, config.ides); spinner.succeed('Capabilities declared (.toh/capabilities.json)'); @@ -381,7 +399,7 @@ async function declareCapabilities(config) { } async function setupMemoryFolder(targetDir) { - const spinner = ora('Setting up Memory System (7 files)...').start(); + const spinner = startSpinner('Setting up Memory System (7 files)...'); const memoryDir = join(targetDir, '.toh', 'memory'); const archiveDir = join(memoryDir, 'archive'); @@ -536,14 +554,24 @@ async function setupMemoryFolder(targetDir) { *Last updated: ${today}* `; - // Write all 7 memory files - await fs.writeFile(join(memoryDir, 'active.md'), activeTemplate); - await fs.writeFile(join(memoryDir, 'summary.md'), summaryTemplate); - await fs.writeFile(join(memoryDir, 'decisions.md'), decisionsTemplate); - await fs.writeFile(join(memoryDir, 'changelog.md'), changelogTemplate); - await fs.writeFile(join(memoryDir, 'agents-log.md'), agentsLogTemplate); - await fs.writeFile(join(memoryDir, 'architecture.md'), architectureTemplate); - await fs.writeFile(join(memoryDir, 'components.md'), componentsTemplate); + // Write all 7 memory files — seed ONLY if absent (v2.1.0). These hold live + // project state; the README promises "reinstalling ... without deleting + // your existing memory", so a reinstall must never clobber them. + const memorySeeds = { + 'active.md': activeTemplate, + 'summary.md': summaryTemplate, + 'decisions.md': decisionsTemplate, + 'changelog.md': changelogTemplate, + 'agents-log.md': agentsLogTemplate, + 'architecture.md': architectureTemplate, + 'components.md': componentsTemplate + }; + for (const [file, content] of Object.entries(memorySeeds)) { + const p = join(memoryDir, file); + if (!fs.existsSync(p)) { + await fs.writeFile(p, content); + } + } // v2.0.0: seed THE TOH LOOP artifacts — ONLY if absent (they hold live // loop state; a reinstall must NEVER clobber an in-flight plan/ledger). @@ -639,10 +667,11 @@ function printNextSteps(config) { if (config.ides.includes('codex') || config.ides.includes('codex-cli')) { console.log(row(chalk.white(pad(' Codex CLI:')))); - // 9 chars green + 51 chars gray = 60 - console.log(row(chalk.green(' codex') + chalk.gray(' - Start Codex CLI in project'.padEnd(51)))); - // 13 chars green + 47 chars gray = 60 - console.log(row(chalk.green(' /toh-vibe') + chalk.gray(' - Create new project'.padEnd(47)))); + // Native skills are the canonical integration; `/toh-*` is compat text. + // 13 green + 47 gray = 60 ; 11 green + 49 gray = 60 ; 18 green + 42 gray = 60 + console.log(row(chalk.green(' $toh-vibe') + chalk.gray(' - Native skill: new project'.padEnd(47)))); + console.log(row(chalk.green(' /skills') + chalk.gray(' - Browse all TOH skills'.padEnd(49)))); + console.log(row(chalk.green(' .codex/skills/') + chalk.gray(' - 14 native skills installed'.padEnd(42)))); console.log(empty); } diff --git a/installer/status.js b/installer/status.js index 84afea7..bf9667c 100644 --- a/installer/status.js +++ b/installer/status.js @@ -40,8 +40,10 @@ export async function status() { const projectPaths = [ { path: join(cwd, '.claude'), name: '.claude/' }, { path: join(cwd, '.cursor', 'rules'), name: '.cursor/rules/' }, + { path: join(cwd, '.codex', 'skills'), name: '.codex/skills/' }, { path: join(cwd, '.toh'), name: '.toh/' }, { path: join(cwd, 'CLAUDE.md'), name: 'CLAUDE.md' }, + { path: join(cwd, 'AGENTS.md'), name: 'AGENTS.md' }, { path: join(cwd, '.cursorrules'), name: '.cursorrules' } ]; diff --git a/installer/uninstall.js b/installer/uninstall.js new file mode 100644 index 0000000..2e7800c --- /dev/null +++ b/installer/uninstall.js @@ -0,0 +1,90 @@ +/** + * Toh Framework Uninstaller + * + * Removes TOH-managed files from a target project. + * + * Scoping rules: + * --ide codex -> only Codex-owned TOH files (.codex/skills/toh-* managed + * skills + the AGENTS.md TOH block). `.toh/` is shared + * runtime state and stays — other IDEs may still use it. + * (no --ide) -> full removal: every IDE surface the installer owns, + * plus `.toh/`. + * + * User content is never deleted: user skills under .codex/skills, user text + * in AGENTS.md outside the TOH markers, and files we cannot classify are + * all left untouched. + */ + +import chalk from 'chalk'; +import ora from 'ora'; +import fs from 'fs-extra'; +import { join } from 'path'; +import { uninstallCodex } from './ide-handlers/codex.js'; + +export async function uninstall(options) { + const targetDir = options.target || process.cwd(); + const ides = options.ide + ? options.ide.split(',').map((i) => i.trim().toLowerCase()) + : null; // null = full uninstall + + console.log(chalk.cyan(`\n🗑️ Uninstalling Toh Framework from ${targetDir}\n`)); + + const isCodex = (i) => i === 'codex' || i === 'codex-cli'; + + if (ides && !ides.every(isCodex)) { + const unsupported = ides.filter((i) => !isCodex(i)); + console.log( + chalk.yellow( + ` ⚠️ Per-IDE uninstall is currently implemented for Codex only ` + + `(got: ${unsupported.join(', ')}).` + ) + ); + console.log(chalk.yellow(' Run without --ide for a full uninstall, or use --ide codex.\n')); + return; + } + + // ---- Codex teardown (per-IDE and full uninstall both do this) ---- + const spinner = ora('Removing Codex integration...').start(); + try { + const { removedSkills, agentsMd } = await uninstallCodex(targetDir); + const parts = []; + parts.push(removedSkills.length ? `${removedSkills.length} skill(s) removed` : 'no TOH skills found'); + if (agentsMd === 'updated') parts.push('AGENTS.md block removed'); + if (agentsMd === 'removed') parts.push('AGENTS.md removed (was TOH-only)'); + spinner.succeed(`Codex integration removed (${parts.join(', ')})`); + } catch (error) { + spinner.fail(`Failed to remove Codex integration: ${error.message}`); + } + + if (!ides) { + // ---- Full uninstall: remaining TOH-owned paths ---- + const fullPaths = [ + join(targetDir, '.toh'), + join(targetDir, '.claude', 'skills'), + join(targetDir, '.claude', 'agents'), + join(targetDir, '.claude', 'commands') + ]; + const fullSpinner = ora('Removing TOH runtime and IDE resources...').start(); + const removed = []; + for (const p of fullPaths) { + if (fs.existsSync(p)) { + await fs.remove(p); + removed.push(p.slice(targetDir.length + 1)); + } + } + fullSpinner.succeed( + removed.length + ? `Removed: ${removed.join(', ')}` + : 'No TOH runtime found' + ); + + console.log( + chalk.gray( + '\n Note: CLAUDE.md / .cursorrules / .gemini / .agent files are user-shared\n' + + ' files without TOH markers — review them manually if you installed those IDEs.' + ) + ); + } + + console.log(chalk.green('\n✅ Uninstall complete.\n')); +} diff --git a/package-lock.json b/package-lock.json index dd8fa13..3ff562f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "toh-framework", - "version": "2.0.0", + "version": "2.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "toh-framework", - "version": "2.0.0", + "version": "2.1.0", "license": "MIT", "dependencies": { "chalk": "^5.3.0", diff --git a/package.json b/package.json index 6833091..6fc3eec 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "toh-framework", - "version": "2.0.0", + "version": "2.1.0", "type": "module", "description": "AI-Orchestration Driven Development - Type Once, Have it all! Approve once and the TOH LOOP builds, tests, and fixes a whole app until verified DONE. For Claude Code, Cursor, Codex, Gemini CLI, and Antigravity.", "author": { @@ -50,7 +50,8 @@ "install:local": "node bin/toh-cli.js install", "list": "node bin/toh-cli.js list", "status": "node bin/toh-cli.js status", - "bundle": "node bin/toh-cli.js bundle" + "bundle": "node bin/toh-cli.js bundle", + "test": "node tests/run.js" }, "engines": { "node": ">=18.0.0" diff --git a/tests/codex.test.js b/tests/codex.test.js new file mode 100644 index 0000000..5982209 --- /dev/null +++ b/tests/codex.test.js @@ -0,0 +1,325 @@ +/** + * Codex integration tests (node:test). + * + * Covers: + * - fresh installation layout (.toh/, .codex/skills/, AGENTS.md) + * - preservation of existing AGENTS.md content + * - idempotency + deterministic output across reinstalls + * - preservation of unrelated user Codex skills + * - removal of stale TOH-managed skills + * - uninstall (TOH files out, user files stay) + * - SKILL.md validity (frontmatter, non-empty body, resolving references) + * + * Run: npm test + */ + +// Silence ora spinners: ora corrupts the node:test child-process IPC channel. +// Reads at call time, so import hoisting is not a problem. +process.env.TOH_QUIET = '1'; + +import { test, before, after } from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'fs-extra'; +import os from 'os'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import yaml from 'js-yaml'; + +import { install } from '../installer/install.js'; +import { + setupCodex, + uninstallCodex, + installCodexSkills, + readCommandCatalog +} from '../installer/ide-handlers/codex.js'; + +const __filename = fileURLToPath(import.meta.url); +const REPO_ROOT = path.join(path.dirname(__filename), '..'); +const SRC_DIR = path.join(REPO_ROOT, 'src'); + +const EXPECTED_SKILLS = [ + 'toh', + 'toh-connect', + 'toh-design', + 'toh-dev', + 'toh-fix', + 'toh-help', + 'toh-line', + 'toh-mobile', + 'toh-plan', + 'toh-protect', + 'toh-ship', + 'toh-test', + 'toh-ui', + 'toh-vibe' +]; + +// ---------------------------------------------------------------- helpers + +async function makeTmpProject() { + return fs.mkdtemp(path.join(os.tmpdir(), 'toh-codex-test-')); +} + +async function quickInstallCodex(targetDir) { + // install() with quick: true is fully non-interactive, even on reinstall. + await install({ target: targetDir, ide: 'codex', quick: true }); +} + +/** Map of relative file path -> content for every file under root. */ +async function snapshotTree(root) { + const out = new Map(); + const walk = async (dir) => { + for (const entry of await fs.readdir(dir, { withFileTypes: true })) { + const p = path.join(dir, entry.name); + if (entry.isDirectory()) await walk(p); + else out.set(path.relative(root, p), await fs.readFile(p, 'utf8')); + } + }; + if (await fs.pathExists(root)) await walk(root); + return out; +} + +function parseSkillFrontmatter(raw) { + const m = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/); + assert.ok(m, 'SKILL.md must start with a YAML frontmatter block'); + return { fm: yaml.load(m[1]), body: m[2] }; +} + +// ---------------------------------------------------------------- tests + +test('fresh install creates .toh/, .codex/skills/ and AGENTS.md', async () => { + const dir = await makeTmpProject(); + try { + await quickInstallCodex(dir); + + assert.ok(await fs.pathExists(path.join(dir, '.toh', 'plan.md')), '.toh/plan.md exists'); + assert.ok(await fs.pathExists(path.join(dir, '.toh', 'progress.md')), '.toh/progress.md exists'); + assert.ok(await fs.pathExists(path.join(dir, '.toh', 'memory', 'active.md')), 'memory seeded'); + assert.ok(await fs.pathExists(path.join(dir, '.toh', 'skills', 'orchestration-protocol', 'SKILL.md')), '.toh skills installed'); + assert.ok(await fs.pathExists(path.join(dir, 'AGENTS.md')), 'AGENTS.md exists'); + + for (const skill of EXPECTED_SKILLS) { + assert.ok( + await fs.pathExists(path.join(dir, '.codex', 'skills', skill, 'SKILL.md')), + `native skill installed: ${skill}` + ); + } + } finally { + await fs.remove(dir); + } +}); + +test('existing AGENTS.md user content is preserved', async () => { + const dir = await makeTmpProject(); + try { + const userContent = '# My Project\n\nKeep this text.\n'; + await fs.writeFile(path.join(dir, 'AGENTS.md'), userContent); + + await quickInstallCodex(dir); + + const agents = await fs.readFile(path.join(dir, 'AGENTS.md'), 'utf8'); + assert.ok(agents.includes('Keep this text.'), 'user text kept'); + assert.ok(agents.includes(''), 'TOH block present'); + assert.ok(agents.indexOf('Keep this text.') < agents.indexOf(''), 'TOH block appended after user content'); + } finally { + await fs.remove(dir); + } +}); + +test('reinstall is idempotent and deterministic', async () => { + const dir = await makeTmpProject(); + try { + await quickInstallCodex(dir); + const first = await snapshotTree(dir); + + await quickInstallCodex(dir); // quick mode must not prompt + const second = await snapshotTree(dir); + + // Exactly one TOH block in AGENTS.md + const agents = second.get('AGENTS.md'); + const starts = agents.split('').length - 1; + const ends = agents.split('').length - 1; + assert.equal(starts, 1, 'exactly one TOH start marker'); + assert.equal(ends, 1, 'exactly one TOH end marker'); + + // Same file set (no duplicated skills) + assert.deepEqual([...first.keys()].sort(), [...second.keys()].sort(), 'file set stable'); + + // Deterministic content for everything except timestamped install metadata + const VOLATILE = new Set(['.toh/manifest.json', '.toh/capabilities.json']); + for (const [file, content] of first) { + if (VOLATILE.has(file)) continue; + assert.equal(second.get(file), content, `identical content: ${file}`); + } + } finally { + await fs.remove(dir); + } +}); + +test('unrelated user Codex skills are never touched', async () => { + const dir = await makeTmpProject(); + try { + const userSkillDir = path.join(dir, '.codex', 'skills', 'my-company-skill'); + await fs.ensureDir(userSkillDir); + const userSkill = '---\nname: my-company-skill\ndescription: mine\n---\n\nUser-owned.\n'; + await fs.writeFile(path.join(userSkillDir, 'SKILL.md'), userSkill); + + await quickInstallCodex(dir); + + assert.equal( + await fs.readFile(path.join(userSkillDir, 'SKILL.md'), 'utf8'), + userSkill, + 'user skill content unchanged' + ); + + // Even a user-owned skill with a toh- name survives (no generator marker). + const userTohDir = path.join(dir, '.codex', 'skills', 'toh-custom'); + await fs.ensureDir(userTohDir); + const userToh = '---\nname: toh-custom\ndescription: user owned\n---\n\nMine.\n'; + await fs.writeFile(path.join(userTohDir, 'SKILL.md'), userToh); + await setupCodex(dir, SRC_DIR, 'en'); + assert.equal(await fs.readFile(path.join(userTohDir, 'SKILL.md'), 'utf8'), userToh, 'user toh-* skill kept'); + } finally { + await fs.remove(dir); + } +}); + +test('stale TOH-managed skills are removed on reinstall', async () => { + const dir = await makeTmpProject(); + try { + await quickInstallCodex(dir); + + // Simulate a skill generated by an older TOH version (has the marker). + const staleDir = path.join(dir, '.codex', 'skills', 'toh-legacy'); + await fs.ensureDir(staleDir); + await fs.writeFile( + path.join(staleDir, 'SKILL.md'), + '---\nname: toh-legacy\ndescription: old\nmetadata:\n generator: toh-framework\n---\n\nOld.\n' + ); + + await setupCodex(dir, SRC_DIR, 'en'); + + assert.ok(!(await fs.pathExists(staleDir)), 'stale TOH-managed skill removed'); + assert.ok(await fs.pathExists(path.join(dir, '.codex', 'skills', 'toh-plan', 'SKILL.md')), 'current skills intact'); + } finally { + await fs.remove(dir); + } +}); + +test('uninstall removes TOH Codex files and keeps user files + .toh state', async () => { + const dir = await makeTmpProject(); + try { + // Pre-existing user content + await fs.writeFile(path.join(dir, 'AGENTS.md'), '# My Project\n\nKeep this text.\n'); + const userSkillDir = path.join(dir, '.codex', 'skills', 'my-company-skill'); + await fs.ensureDir(userSkillDir); + await fs.writeFile(path.join(userSkillDir, 'SKILL.md'), '---\nname: my-company-skill\ndescription: mine\n---\n'); + + await quickInstallCodex(dir); + + // Pretend the loop ran: live state that must survive a codex uninstall. + await fs.writeFile(path.join(dir, '.toh', 'plan.md'), '# Plan: real work\n\n- [ ] T001 [P] ui-builder — thing in app/page.tsx\n'); + + const { removedSkills, agentsMd } = await uninstallCodex(dir); + + assert.equal(removedSkills.length, EXPECTED_SKILLS.length, 'all TOH skills removed'); + for (const skill of EXPECTED_SKILLS) { + assert.ok(!(await fs.pathExists(path.join(dir, '.codex', 'skills', skill))), `removed ${skill}`); + } + assert.ok(await fs.pathExists(path.join(userSkillDir, 'SKILL.md')), 'user skill remains'); + assert.ok(await fs.pathExists(path.join(dir, '.codex', 'skills')), '.codex/skills dir kept'); + + assert.equal(agentsMd, 'updated'); + const agents = await fs.readFile(path.join(dir, 'AGENTS.md'), 'utf8'); + assert.ok(agents.includes('Keep this text.'), 'user AGENTS.md text remains'); + assert.ok(!agents.includes(''), 'TOH block removed'); + + // Codex-only uninstall must not touch shared .toh state. + const plan = await fs.readFile(path.join(dir, '.toh', 'plan.md'), 'utf8'); + assert.ok(plan.includes('real work'), '.toh/plan.md preserved'); + } finally { + await fs.remove(dir); + } +}); + +test('uninstall deletes AGENTS.md only when it was TOH-only', async () => { + const dir = await makeTmpProject(); + try { + await setupCodex(dir, SRC_DIR, 'en'); // standalone: creates AGENTS.md + const { agentsMd } = await uninstallCodex(dir); + assert.equal(agentsMd, 'removed'); + assert.ok(!(await fs.pathExists(path.join(dir, 'AGENTS.md'))), 'TOH-only AGENTS.md removed'); + } finally { + await fs.remove(dir); + } +}); + +test('every generated SKILL.md is valid and its references resolve', async () => { + const dir = await makeTmpProject(); + try { + await quickInstallCodex(dir); + + const skillsRoot = path.join(dir, '.codex', 'skills'); + const entries = (await fs.readdir(skillsRoot, { withFileTypes: true })).filter((e) => e.isDirectory()); + assert.equal(entries.length, EXPECTED_SKILLS.length, 'exactly the TOH skills exist'); + + const NAME_RE = /^[a-z0-9-]{1,64}$/; + for (const entry of entries) { + const raw = await fs.readFile(path.join(skillsRoot, entry.name, 'SKILL.md'), 'utf8'); + const { fm, body } = parseSkillFrontmatter(raw); + + assert.ok(NAME_RE.test(fm.name), `${entry.name}: valid skill name`); + assert.equal(fm.name, entry.name, 'frontmatter name matches directory'); + assert.ok(typeof fm.description === 'string' && fm.description.length > 0, 'description present'); + assert.ok(fm.description.length <= 1024, 'description within Codex limit'); + assert.equal(fm.metadata?.generator, 'toh-framework', 'generator marker present'); + assert.ok(body.trim().length > 0, 'non-empty instructions'); + + // Every `.toh/...` reference in the body must resolve to a real file. + const refs = raw.match(/`(\.toh\/[^`]+)`/g) || []; + assert.ok(refs.length > 0, `${entry.name}: references .toh files`); + for (const ref of refs) { + const rel = ref.slice(1, -1); + assert.ok(await fs.pathExists(path.join(dir, rel)), `${entry.name}: resolves ${rel}`); + } + } + } finally { + await fs.remove(dir); + } +}); + +test('skills reference the supporting .toh/skills from command frontmatter', async () => { + const dir = await makeTmpProject(); + try { + await quickInstallCodex(dir); + const catalog = await readCommandCatalog(SRC_DIR); + for (const entry of catalog) { + const raw = await fs.readFile( + path.join(dir, '.codex', 'skills', entry.skillName, 'SKILL.md'), + 'utf8' + ); + assert.ok(raw.includes(`.toh/commands/${entry.file}`), `${entry.skillName}: references its command file`); + for (const s of entry.skills) { + assert.ok(raw.includes(`.toh/skills/${s}/SKILL.md`), `${entry.skillName}: references skill ${s}`); + assert.ok( + await fs.pathExists(path.join(dir, '.toh', 'skills', s, 'SKILL.md')), + `${entry.skillName}: supporting skill ${s} actually installed` + ); + } + } + } finally { + await fs.remove(dir); + } +}); + +test('catalog parsing fails with an actionable error on a broken package', async () => { + const dir = await makeTmpProject(); + try { + await assert.rejects( + () => installCodexSkills(dir, path.join(dir, 'no-such-src')), + /command source not found/i + ); + } finally { + await fs.remove(dir); + } +}); diff --git a/tests/run.js b/tests/run.js new file mode 100644 index 0000000..eb20b1e --- /dev/null +++ b/tests/run.js @@ -0,0 +1,13 @@ +/** + * In-band test runner. + * + * Why not `node --test tests/`? Node's default test isolation spawns each + * test file as a child process that reports over an IPC channel. On Node 24 + * that channel is intermittently corrupted by this repo's dependency stack + * ("Unable to deserialize cloned data due to invalid or unsupported + * version") even though every test passes when run in-band. Importing the + * test files here runs node:test in-process — deterministic on Node >= 18, + * and the process exit code still reflects failures. + */ + +import './codex.test.js'; From 9be34d34491337a40465a5d3810c82593baea25a Mon Sep 17 00:00:00 2001 From: Patipat Chewprecha Date: Mon, 17 Aug 2026 09:13:07 +0700 Subject: [PATCH 2/4] refactor: update Codex integration to use .agents directory and enhance uninstall process - Changed installation paths from .codex/skills to .agents/skills for better organization. - Updated uninstall logic to handle new paths and added dry-run functionality. - Enhanced AGENTS.md handling to ensure size limits are enforced and backups are created. - Modified tests to reflect changes in directory structure and functionality. - Improved documentation to clarify the new structure and usage. --- CHANGELOG.md | 10 +- CLAUDE.md | 8 +- README.md | 10 +- bin/toh-cli.js | 2 + docs/README-TH.md | 6 +- installer/ide-handlers/codex.js | 638 ++++++++++++++++++-------------- installer/install.js | 9 +- installer/status.js | 3 +- installer/uninstall.js | 22 +- src/agents/README.md | 4 +- tests/codex.test.js | 130 +++++-- 11 files changed, 503 insertions(+), 339 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bbc4f3..cd33898 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,28 +6,28 @@ All notable changes to Toh Framework will be documented in this file. ### 🧠 Native Codex Skills Integration -The Codex integration no longer simulates `/toh-*` slash commands through a giant `AGENTS.md` — Codex has no custom slash commands, so that recognition was unreliable. TOH workflows now install as **native Codex skills** (`.codex/skills//SKILL.md`), which Codex discovers from the project root and can invoke explicitly (`$toh-vibe`, `/skills`) or match implicitly from the task description. +The Codex integration no longer simulates `/toh-*` slash commands through a giant `AGENTS.md` — Codex has no custom slash commands, so that recognition was unreliable. TOH now installs **native Codex skill wrappers** under `.agents/skills/`, which Codex discovers from the repository and can invoke explicitly (`$toh-vibe`, `/skills`) or match implicitly from the task description. #### Changed - **Codex handler rewritten** (`installer/ide-handlers/codex.js`) - now installs one thin native skill per TOH command (14 skills), each a wrapper that points at the real workflow in `.toh/commands/*.md` and the supporting skills in `.toh/skills/` — no duplicated workflow content. Codex constraints (no subagents, no Stop hook, no model routing) are stated explicitly with sequential fallbacks. - **`AGENTS.md` block slimmed** - the managed `` block now carries only project-level rules: identity, capabilities, the native-skills table, legacy `/toh-*` compatibility note, `.toh` runtime map, and the tiered memory protocol. The ~800-line embedded copy of every agent body is gone (Codex has no subagents to run them). -- **Codex install output** - the success box now points at `$toh-vibe` / `/skills` and `.codex/skills/` instead of implying `/toh-*` is registered. +- **Codex install output** - the success box now points at `$toh-vibe` / `/skills` and `.agents/skills/` instead of implying `/toh-*` is registered. - **Reinstall safety** - `.toh/memory/*.md` are now seeded only if absent (previously every reinstall overwrote them, contradicting the README's "without deleting your existing memory" promise); `plan.md` / `progress.md` were already protected. - **`--quick` reinstalls are non-interactive** - an existing install under `--quick` now defaults to Quick Update instead of prompting (interactive behavior unchanged). #### Added -- **Native Codex skills** - `.codex/skills/toh/SKILL.md` + one per `/toh-*` command, generated deterministically from `src/commands/*.md` frontmatter (single source of truth), with a `metadata.generator: toh-framework` marker. +- **Native Codex skills** - 23 supporting-skill wrappers + 14 command wrappers under `.agents/skills/`, generated from `src/skills/` and `src/commands/*.md` with a `metadata.generator: toh-framework` marker. - **Stale-skill cleanup** - reinstall removes previously TOH-generated skills that no longer exist, identified by the generator marker; user skills (even user skills named `toh-*`) are never touched. - **`toh uninstall`** - new CLI command. `--ide codex` removes only TOH-managed Codex files (skills + AGENTS.md block, user content preserved); without `--ide` it also removes `.toh` and the other TOH-owned IDE resource dirs. The installer's Fresh Install cleanup uses the same Codex teardown. -- **`toh status`** - now reports `.codex/skills/` and `AGENTS.md`. +- **`toh status`** - now reports `.agents/skills/`, `.codex/config.toml`, and `AGENTS.md`. - **Test suite** - `tests/codex.test.js` (node:test, in-band runner via `tests/run.js`) covers fresh install, AGENTS.md preservation, idempotency/determinism, user-skill preservation, stale-skill removal, uninstall scoping, and SKILL.md validity with resolving references. Runs in CI (`npm test`) on Node 18 + 22. #### Technical - Agent count (**8**), skill count (**23**), command count (**14**) unchanged — Codex skills are generated wrappers, not new content. -- Synced IDE surfaces: Codex (`.codex/skills/` + `AGENTS.md` block), README.md, docs/README-TH.md, installer output, `toh status`. +- Synced IDE surfaces: Codex (`.agents/skills/` + `.codex/config.toml` + `AGENTS.md` block), README.md, docs/README-TH.md, installer output, `toh status`. - Version bumped from **2.0.0** to **2.1.0**. --- diff --git a/CLAUDE.md b/CLAUDE.md index 25daa9e..7d1b2cc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -31,7 +31,7 @@ All npm scripts wrap `node bin/toh-cli.js `, which lazy-loads `installer/*. (.github/workflows/ci.yml runs both on Node 18 + 22): 1. Run what you touched — `npm test`, install into a scratch dir, `npm run list`/`status`, `npm pack --dry-run`. - Inspect the generated output (.toh/, .claude/, .cursor/rules/, AGENTS.md, .codex/skills/, .gemini/, .agent/workflows/) — never assume a transform worked. + Inspect the generated output (.toh/, .claude/, .cursor/rules/, AGENTS.md, .agents/skills/, .codex/config.toml, .gemini/, .agent/workflows/) — never assume a transform worked. 2. Coffee-Shop-Owner Test for any user-facing change: Could a coffee-shop owner use this without tech vocabulary? Does the system ever ask a question they can't answer? When it breaks, do they know what to do next? Does the output look professionally made? @@ -54,9 +54,9 @@ then 4 handlers in `installer/ide-handlers/` (plus shared.js utilities) cover th - gemini-cli.js → BOTH Gemini CLI (.gemini/: TOML commands from src/gemini-commands/, skills, GEMINI.md) and Antigravity (.agent/workflows/ from src/antigravity-workflows/); selecting gemini auto-adds antigravity. There is no antigravity.js. -- codex.js → NATIVE Codex skills: one thin wrapper per command at - .codex/skills//SKILL.md (generated from src/commands/ frontmatter; each wrapper - points at .toh/commands/*.md + .toh/skills/* and states Codex constraints — no subagents, +- codex.js → NATIVE Codex skills: one thin wrapper per supporting skill and command at + .agents/skills//SKILL.md (generated from src/skills/ and src/commands/; each wrapper + points at .toh/commands/*.md or .toh/skills/* and states Codex constraints — no subagents, no Stop hook, sequential TOH LOOP) + a CONCISE managed AGENTS.md block (TOH-FRAMEWORK-START/END: identity, capabilities, skills table, legacy `/toh-*` compat note, memory protocol). Never embed agent bodies in AGENTS.md again (pre-v2.1 behavior — diff --git a/README.md b/README.md index e3dbe8b..e834c09 100644 --- a/README.md +++ b/README.md @@ -182,9 +182,11 @@ gemini ### Codex CLI Codex has no custom slash commands, so TOH installs **native Codex skills** -under `.codex/skills/` — one per TOH workflow (`toh-vibe`, `toh-plan`, -`toh-ui`, `toh-dev`, `toh-design`, `toh-test`, `toh-connect`, `toh-line`, -`toh-mobile`, `toh-fix`, `toh-ship`, `toh-protect`, `toh-help`, `toh`). +under `.agents/skills/` — 23 supporting-skill wrappers plus 14 workflow +wrappers (`toh-vibe`, `toh-plan`, `toh-ui`, `toh-dev`, `toh-design`, +`toh-test`, `toh-connect`, `toh-line`, `toh-mobile`, `toh-fix`, `toh-ship`, +`toh-protect`, `toh-help`, `toh`). Codex project-doc quota is configured in +`.codex/config.toml`. ```bash # Open the project root in Codex @@ -336,7 +338,7 @@ claude -p "/toh-vibe coffee shop management system" --permission-mode acceptEdit | 🇹🇭 Thai documentation | [docs/README-TH.md](docs/README-TH.md) | | Full version history | [CHANGELOG.md](CHANGELOG.md) | | All commands + cheatsheet | run `/toh-help` in your IDE | -| Per-project guide (auto-generated) | `CLAUDE.md` / `AGENTS.md` / `GEMINI.md` / `.cursor/rules` / `.codex/skills` in your project after install | +| Per-project guide (auto-generated) | `CLAUDE.md` / `AGENTS.md` / `GEMINI.md` / `.cursor/rules` / `.agents/skills` in your project after install | | The plan artifact | `.toh/plan.md` — your app's live checklist (open it anytime to see progress) | | Design contract | `DESIGN.md` at your project root — generated per project, edit it to steer the look | diff --git a/bin/toh-cli.js b/bin/toh-cli.js index d730b5f..a9c73bf 100755 --- a/bin/toh-cli.js +++ b/bin/toh-cli.js @@ -71,6 +71,8 @@ program .description('Remove Toh Framework from your project (keeps user files)') .option('-t, --target ', 'Target directory', process.cwd()) .option('-i, --ide ', 'IDE to remove (currently: codex). Omit for full uninstall') + .option('--dry-run', 'Preview owned files without removing them') + .option('--no-backup', 'Do not create a backup before removal') .action(async (options) => { const { uninstall } = await import('../installer/uninstall.js'); await uninstall(options); diff --git a/docs/README-TH.md b/docs/README-TH.md index 4aa7ee1..dd0bbfe 100644 --- a/docs/README-TH.md +++ b/docs/README-TH.md @@ -165,10 +165,12 @@ gemini ### Codex CLI Codex ไม่มี slash command แบบกำหนดเอง ดังนั้น TOH จะติดตั้ง **native Codex -skills** ไว้ที่ `.codex/skills/` — หนึ่ง skill ต่อหนึ่งเวิร์กโฟลว์ +skills** ไว้ที่ `.agents/skills/` — wrapper ของ supporting skill 23 ตัวและ +workflow 14 ตัว (`toh-vibe`, `toh-plan`, `toh-ui`, `toh-dev`, `toh-design`, `toh-test`, `toh-connect`, `toh-line`, `toh-mobile`, `toh-fix`, `toh-ship`, -`toh-protect`, `toh-help`, `toh`) +`toh-protect`, `toh-help`, `toh`) โดย quota ของเอกสารโปรเจคอยู่ใน +`.codex/config.toml` ```bash # เปิดโฟลเดอร์โปรเจคใน Codex diff --git a/installer/ide-handlers/codex.js b/installer/ide-handlers/codex.js index 88e0342..40270ff 100644 --- a/installer/ide-handlers/codex.js +++ b/installer/ide-handlers/codex.js @@ -1,20 +1,11 @@ /** * Codex CLI IDE Handler * - * Native Codex integration (v2.1.0): - * .codex/skills//SKILL.md -> native Codex skills (the canonical layer, - * discovered by Codex from the project root) - * AGENTS.md (managed block) -> concise project-level TOH rules + - * legacy `/toh-*` compatibility note - * .toh/ -> TOH runtime/state (plan, progress, memory, - * skills, commands) — owned by install.js - * - * Codex has no custom slash commands, subagents, or Stop hooks, so the old - * "simulate /toh-* via a giant AGENTS.md" approach was unreliable. Skills are - * the supported discovery/invocation mechanism; AGENTS.md now only carries - * project-level orchestration rules inside a TOH-managed marker block. + * Codex discovers repository skills from .agents/skills. TOH keeps the full + * runtime in .toh/ and installs thin wrappers for commands and skills. */ +import crypto from 'crypto'; import fs from 'fs-extra'; import path from 'path'; import { fileURLToPath } from 'url'; @@ -26,27 +17,74 @@ const __dirname = path.dirname(__filename); const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '../../package.json'), 'utf-8')); const VERSION = pkg.version; +export const CODEX_SKILLS_DIR = path.join('.agents', 'skills'); +export const AGENTS_MAX_BYTES = 24 * 1024; +export const CODEX_PROJECT_DOC_MAX_BYTES = 64 * 1024; + const AGENTS_BLOCK_START = ''; const AGENTS_BLOCK_END = ''; const AGENTS_BLOCK_RE = /[ \t]*[\s\S]*?[ \t]*\r?\n?/g; - -// Generated Codex skills carry this frontmatter marker so we can tell -// TOH-managed skills apart from a user's own skills on reinstall/uninstall. +const CONFIG_BLOCK_START = '# TOH-FRAMEWORK-START'; +const CONFIG_BLOCK_END = '# TOH-FRAMEWORK-END'; +const CONFIG_BLOCK_RE = /[ \t]*# TOH-FRAMEWORK-START\r?\n[\s\S]*?[ \t]*# TOH-FRAMEWORK-END[ \t]*\r?\n?/g; const SKILL_GENERATOR = 'toh-framework'; - -// Codex skill names: 1-64 chars, lowercase letters, numbers, hyphens. +const MANIFEST_PATH = path.join('.codex', 'toh-framework.json'); const SKILL_NAME_RE = /^[a-z0-9-]{1,64}$/; -// ============================================================ -// Command catalog (single source: src/commands/*.md frontmatter) -// ============================================================ +function sha256(value) { + return crypto.createHash('sha256').update(value).digest('hex'); +} + +function relativePath(...parts) { + return path.posix.join(...parts.map((part) => String(part).replaceAll(path.sep, '/'))); +} + +function skillFileRelativePath(name) { + return relativePath(CODEX_SKILLS_DIR, name, 'SKILL.md'); +} + +async function readManifest(targetDir) { + const manifestPath = path.join(targetDir, MANIFEST_PATH); + if (!(await fs.pathExists(manifestPath))) { + return { generator: SKILL_GENERATOR, version: VERSION, files: {} }; + } + + try { + const manifest = await fs.readJson(manifestPath); + if (manifest.generator !== SKILL_GENERATOR || typeof manifest.files !== 'object') { + return { generator: SKILL_GENERATOR, version: VERSION, files: {} }; + } + return manifest; + } catch { + return { generator: SKILL_GENERATOR, version: VERSION, files: {} }; + } +} + +async function writeManifest(targetDir, manifest) { + const manifestPath = path.join(targetDir, MANIFEST_PATH); + await fs.ensureDir(path.dirname(manifestPath)); + await fs.writeJson(manifestPath, manifest, { spaces: 2 }); +} + +async function fileMatchesHash(filePath, expectedHash) { + if (!expectedHash || !(await fs.pathExists(filePath))) return false; + try { + return sha256(await fs.readFile(filePath)) === expectedHash; + } catch { + return false; + } +} + +function parseFrontmatter(raw, label) { + const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/); + if (!match) throw new Error(`${label} must start with YAML frontmatter.`); + try { + return yaml.load(match[1]) || {}; + } catch (error) { + throw new Error(`Invalid YAML frontmatter in ${label}: ${error.message}`); + } +} -/** - * Read src/commands/*.md and return one entry per TOH command: - * { skillName, command, aliases, description, skills, file } - * Sorted by skillName for deterministic output. Throws an actionable error - * when the catalog cannot be read (the skills layer depends on it). - */ export async function readCommandCatalog(srcDir) { const commandsDir = path.join(srcDir, 'commands'); if (!(await fs.pathExists(commandsDir))) { @@ -54,27 +92,28 @@ export async function readCommandCatalog(srcDir) { } const files = (await fs.readdir(commandsDir)) - .filter((f) => f.endsWith('.md') && f !== 'README.md') + .filter((file) => file.endsWith('.md') && file !== 'README.md') .sort(); - const catalog = []; + for (const file of files) { const raw = await fs.readFile(path.join(commandsDir, file), 'utf8'); - const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/); - if (!fmMatch) continue; // no frontmatter -> not a command definition + const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/); + if (!match) continue; let parsed; try { - parsed = yaml.load(fmMatch[1]) || {}; - } catch (err) { - throw new Error(`Invalid YAML frontmatter in src/commands/${file}: ${err.message}`); + parsed = yaml.load(match[1]) || {}; + } catch (error) { + throw new Error(`Invalid YAML frontmatter in src/commands/${file}: ${error.message}`); } const command = String(parsed.command || '').trim(); const skillName = command.replace(/^\//, ''); - if (!SKILL_NAME_RE.test(skillName)) continue; // not a slash command -> skip + if (!SKILL_NAME_RE.test(skillName)) continue; catalog.push({ + kind: 'command', skillName, command, aliases: Array.isArray(parsed.aliases) ? parsed.aliases.map(String) : [], @@ -90,247 +129,248 @@ export async function readCommandCatalog(srcDir) { return catalog; } -// ============================================================ -// Codex skill generation -// ============================================================ +export async function readSupportingSkillCatalog(srcDir) { + const skillsDir = path.join(srcDir, 'skills'); + if (!(await fs.pathExists(skillsDir))) { + throw new Error(`TOH skill source not found: ${skillsDir} — is this a complete toh-framework package?`); + } -/** - * Build the SKILL.md body for one command. Deterministic: no timestamps. - * Paths are project-root relative (Codex runs from the project root). - */ -function renderSkillMd(entry) { - const triggers = [entry.command, ...entry.aliases].map((c) => `\`${c}\``).join(', '); - // Description rules (Codex): 1-1024 chars, key use case + trigger words - // front-loaded so implicit matching survives description shortening. - const description = - `${entry.description} — TOH Framework workflow (${entry.command}). ` + - `Trigger words: ${[entry.command, ...entry.aliases].join(', ')}.`.slice(0, 1024); + const catalog = []; + const entries = (await fs.readdir(skillsDir, { withFileTypes: true })) + .filter((entry) => entry.isDirectory()) + .sort((a, b) => a.name.localeCompare(b.name)); + + for (const entry of entries) { + const file = path.join(skillsDir, entry.name, 'SKILL.md'); + if (!(await fs.pathExists(file))) continue; + const raw = await fs.readFile(file, 'utf8'); + const frontmatter = raw.startsWith('---\n') || raw.startsWith('---\r\n') + ? parseFrontmatter(raw, file) + : {}; + const skillName = String(frontmatter.name || entry.name).trim(); + if (!SKILL_NAME_RE.test(skillName) || skillName !== entry.name) continue; + const heading = raw.match(/^#\s+(.+)$/m)?.[1]?.trim(); + const description = String(frontmatter.description || heading || `${entry.name} TOH supporting skill`).trim(); + catalog.push({ + kind: 'skill', + skillName, + description, + file: relativePath('skills', entry.name, 'SKILL.md'), + sourcePath: `.toh/skills/${entry.name}/SKILL.md` + }); + } + + if (catalog.length === 0) { + throw new Error(`No TOH supporting skills found in ${skillsDir} — cannot generate Codex skills.`); + } + return catalog; +} +async function readWrapperCatalog(srcDir) { + const [commands, skills] = await Promise.all([ + readCommandCatalog(srcDir), + readSupportingSkillCatalog(srcDir) + ]); + const wrappers = [ + ...skills, + ...commands.map((entry) => ({ + ...entry, + sourcePath: `.toh/commands/${entry.file}` + })) + ].sort((a, b) => a.skillName.localeCompare(b.skillName)); + + const names = new Set(); + for (const wrapper of wrappers) { + if (names.has(wrapper.skillName)) throw new Error(`Duplicate Codex skill name: ${wrapper.skillName}`); + names.add(wrapper.skillName); + } + return { commands, skills, wrappers }; +} + +function wrapperFrontmatter(entry) { + return yaml.dump({ + name: entry.skillName, + description: entry.description.slice(0, 1024), + metadata: { + generator: SKILL_GENERATOR, + version: VERSION, + kind: entry.kind, + source: entry.sourcePath + } + }, { lineWidth: -1, noRefs: true }).trimEnd(); +} + +function renderCommandWrapper(entry) { + const triggers = [entry.command, ...entry.aliases].map((command) => `\`${command}\``).join(', '); const supporting = entry.skills.length ? `2. Read every supporting skill BEFORE executing:\n${entry.skills - .map((s) => ` - \`.toh/skills/${s}/SKILL.md\``) + .map((skill) => ` - \`.toh/skills/${skill}/SKILL.md\``) .join('\n')}\n3. Execute the workflow in this session, in order.` : '2. Execute the workflow in this session, in order.'; - return `--- -name: ${entry.skillName} -description: ${yaml.dump(description, { lineWidth: -1, noRefs: true }).trim()} -metadata: - generator: ${SKILL_GENERATOR} - version: ${VERSION} ---- - -# ${entry.command} — ${entry.description} - -> Codex-native wrapper for the TOH Framework workflow ${entry.command}. -> Generated by ${SKILL_GENERATOR} — do not edit by hand; re-run -> \`npx toh-framework install --ide codex\` to update. -> All paths below are relative to the project root (the directory holding \`.codex/\`). - -## When to use - -${entry.description}. Triggers: ${triggers}, or any plain-language request that matches. - -## Workflow - -1. Read the full workflow definition: \`.toh/commands/${entry.file}\` -${supporting} - -If \`.toh/commands/${entry.file}\` is missing (TOH commands component not -installed), follow the supporting skills directly — they carry the same rules. - -## Codex constraints - -- **No subagents/teams** — where the workflow says "spawn" or "delegate", do - that work inline, one task at a time, in this session. -- **No Claude Code Stop hook** — self-enforce THE TOH LOOP: do not end the run - while \`.toh/plan.md\` has unchecked, unblocked tasks. -- **No model routing** — ignore haiku/sonnet/opus tiers mentioned in TOH docs. -- **State** — persist everything under \`.toh/\` (\`plan.md\`, \`progress.md\`, - \`memory/\`); resume = continue at the first unchecked \`[ ]\` task. + return `---\n${wrapperFrontmatter(entry)}\n---\n\n# ${entry.command} — ${entry.description}\n\n> Thin Codex wrapper for the TOH Framework workflow ${entry.command}.\n> Read the runtime workflow from \`${entry.sourcePath}\`; do not duplicate it here.\n\n## When to use\n\n${entry.description}. Triggers: ${triggers}, or any plain-language request that matches.\n\n## Workflow\n\n1. Read the full workflow definition: \`${entry.sourcePath}\`\n${supporting}\n\n## Codex constraints\n\n- **No subagents or teams** — execute delegated work inline, one task at a time.\n- **No Stop hook** — do not end while \`.toh/plan.md\` has unchecked, unblocked tasks.\n- **No model routing** — ignore model tiers in TOH docs.\n- **State** — persist work under \`.toh/\` and resume from the first unchecked task.\n`; +} -## Legacy command text +function renderSupportingSkillWrapper(entry) { + return `---\n${wrapperFrontmatter(entry)}\n---\n\n# ${entry.skillName}\n\nThis is a thin Codex discovery wrapper for the TOH supporting skill.\nRead the complete instructions from \`${entry.sourcePath}\` before acting.\n\nThe runtime copy under \`.toh/skills/\` is the source of truth; this wrapper exists\nonly so Codex can discover and invoke the skill from the repository skill path.\n`; +} -If the user typed ${triggers} as text: that is this skill. \`/toh-*\` is -compatibility text interpreted via AGENTS.md, not a native Codex slash command. -`; +function renderWrapper(entry) { + return entry.kind === 'command' + ? renderCommandWrapper(entry) + : renderSupportingSkillWrapper(entry); } -/** - * Install .codex/skills//SKILL.md for every command in the catalog. - * - writes are idempotent (same input -> same bytes) - * - stale TOH-managed skills (generator marker, no longer in catalog) are removed - * - user skills (including user skills named toh-*) are never touched - * Returns the list of installed skill names. - */ export async function installCodexSkills(targetDir, srcDir) { - const catalog = await readCommandCatalog(srcDir); - const skillsRoot = path.join(targetDir, '.codex', 'skills'); + const { wrappers } = await readWrapperCatalog(srcDir); + const skillsRoot = path.join(targetDir, CODEX_SKILLS_DIR); await fs.ensureDir(skillsRoot); - - const wanted = new Set(catalog.map((c) => c.skillName)); - - // Remove stale TOH-managed skills (identified by the generator marker — - // never by directory name alone, so user skills are safe). - for (const entry of await fs.readdir(skillsRoot, { withFileTypes: true })) { - if (!entry.isDirectory() || wanted.has(entry.name)) continue; - const skillFile = path.join(skillsRoot, entry.name, 'SKILL.md'); - if (!(await fs.pathExists(skillFile))) continue; - try { - const head = (await fs.readFile(skillFile, 'utf8')).slice(0, 4096); - if (head.includes(`generator: ${SKILL_GENERATOR}`)) { - await fs.remove(path.join(skillsRoot, entry.name)); - } - } catch { - // Unreadable file -> leave it alone; never delete what we can't classify. + const previous = await readManifest(targetDir); + const nextFiles = {}; + const wanted = new Set(wrappers.map((entry) => skillFileRelativePath(entry.skillName))); + + for (const [relative, record] of Object.entries(previous.files || {})) { + if (!relative.startsWith(`${CODEX_SKILLS_DIR}/`) || wanted.has(relative)) continue; + const filePath = path.join(targetDir, relative); + if (await fileMatchesHash(filePath, record.sha256)) { + await fs.remove(filePath); + const parent = path.dirname(filePath); + if ((await fs.readdir(parent)).length === 0) await fs.remove(parent); } } - for (const entry of catalog) { - const dir = path.join(skillsRoot, entry.skillName); - await fs.ensureDir(dir); - await fs.writeFile(path.join(dir, 'SKILL.md'), renderSkillMd(entry)); + const installed = []; + for (const entry of wrappers) { + const relative = skillFileRelativePath(entry.skillName); + const filePath = path.join(targetDir, relative); + const content = renderWrapper(entry); + const existingRecord = previous.files?.[relative]; + const canReplace = !(await fs.pathExists(filePath)) || + await fileMatchesHash(filePath, existingRecord?.sha256); + + if (!canReplace) continue; + await fs.ensureDir(path.dirname(filePath)); + await fs.writeFile(filePath, content); + nextFiles[relative] = { + sha256: sha256(content), + kind: entry.kind, + source: entry.sourcePath + }; + installed.push(entry.skillName); } - return catalog.map((c) => c.skillName); + await writeManifest(targetDir, { + ...previous, + generator: SKILL_GENERATOR, + version: VERSION, + files: nextFiles + }); + return installed; } -// ============================================================ -// AGENTS.md (managed block only) -// ============================================================ - -function renderSkillTable(catalog) { - const rows = catalog - .map((c) => `| \`$${c.skillName}\` | ${c.description} |`) +function renderSkillTable(wrappers) { + const rows = wrappers + .map((entry) => `| \`$${entry.skillName}\` | ${entry.description} |`) .join('\n'); - return `| Skill | Use it to | -|-------|-----------| -${rows}`; + return `| Skill | Use it to |\n|-------|-----------|\n${rows}`; } -function generateAgentsMdEN(catalog) { +function generateAgentsBlock(wrappers, language) { + const capabilities = renderCapabilitiesSection('codex'); + const thai = language === 'th'; + const title = thai + ? 'คุณคือ **Toh Framework Agent** ที่รันอยู่บน **Codex CLI** — ช่วย Solo Developer สร้าง SaaS คนเดียวจนจบ' + : 'You are the **Toh Framework Agent** running in **Codex CLI**, helping solo developers build SaaS systems by themselves.'; + const intro = thai + ? 'เวิร์กโฟลว์และกฎของ TOH ถูกติดตั้งเป็น native skills ไว้ที่ `.agents/skills/`:' + : 'TOH workflows and supporting rules are installed as native skills under `.agents/skills/`:'; + const runtime = thai + ? '- `.toh/plan.md` + `.toh/progress.md` — แผนคือไฟล์และใช้ resume จาก task แรกที่ยังไม่ติ๊ก\n- `.toh/memory/` — memory 7 ไฟล์' + : '- `.toh/plan.md` + `.toh/progress.md` — the plan is a file; resume from the first unchecked task\n- `.toh/memory/` — 7-file memory'; + const constraints = thai + ? '- **ไม่มี subagents/teams** — รัน THE TOH LOOP แบบ sequential ใน session นี้\n- **ไม่มี Stop hook** — ห้ามจบ session ถ้าแผนยังมี task ที่ไม่ได้ติ๊กและไม่ blocked\n- **ไม่มี model routing** — ข้าม tier haiku/sonnet/opus ในเอกสาร TOH' + : '- **No subagents or teams** — run THE TOH LOOP sequentially in this session\n- **No Stop hook** — never end while `.toh/plan.md` has unchecked, unblocked tasks\n- **No model routing** — ignore haiku/sonnet/opus tiers in TOH docs'; + const compatibility = thai + ? '- เรียกตรงๆ ด้วย `$` หรือพิมพ์ `/skills` เพื่อดูทั้งหมด\n- แต่ละ wrapper อ่านเนื้อหาจริงจาก `.toh/commands/` หรือ `.toh/skills/`\n- ถ้าพิมพ์ `/toh-*` ให้ตีความเป็นคำขอใช้ skill ที่ตรงกัน ไม่ใช่ custom slash command' + : '- Invoke explicitly with `$` or browse with `/skills`, or describe the task for implicit matching.\n- Each wrapper reads the full source from `.toh/commands/` or `.toh/skills/`.\n- If the user types `/toh-*`, interpret it as a request for the matching skill, not a custom slash command.'; + return `${AGENTS_BLOCK_START} # 🎯 Toh Framework -> **"Type Once, Have it all!"** — AI-Orchestration Driven Development +> **"Type Once, Have it all!"** — AI-Orchestration Driven Development${thai ? '\n> "สั่งครั้งเดียว จบครบโดยไม่ต้องถาม"' : ''} ## Identity -You are the **Toh Framework Agent** running in **Codex CLI**, helping solo developers build SaaS systems by themselves. +${title} -${renderCapabilitiesSection('codex')} +${capabilities} -## Using TOH in Codex (native skills) +## ${thai ? 'การใช้ TOH ใน Codex (native skills)' : 'Using TOH in Codex (native skills)'} -TOH workflows are installed as **native Codex skills** under \`.codex/skills/\` — this is the canonical integration: +${intro} -${renderSkillTable(catalog)} +${renderSkillTable(wrappers)} -- Invoke explicitly with \`$\` (or browse with \`/skills\`), or describe the task — Codex matches skills by description. -- Each skill reads its workflow from \`.toh/commands/\` and supporting rules from \`.toh/skills/\`. - -### Legacy \`/toh-*\` compatibility text - -Codex has no custom slash commands. If the user types \`/toh-plan\` or \`toh plan\`, interpret it as a request to use the matching skill in \`.codex/skills/\` — compatibility behavior, not a native command. +${compatibility} ## TOH runtime & state (\`.toh/\`) -- \`.toh/plan.md\` + \`.toh/progress.md\` — the plan IS a file; resume = first unchecked \`[ ]\` task -- \`.toh/memory/\` — 7-file memory (protocol below) +${runtime} - \`.toh/skills/\` · \`.toh/commands/\` · \`.toh/capabilities.json\` -## Codex constraints (vs Claude Code) - -- **No subagents/teams** — run THE TOH LOOP sequentially in this session (see \`.toh/skills/orchestration-protocol/SKILL.md\`): implement → run the task's checkpoint → quote real output → fix if red (max 5 tries; 3 consecutive failures = \`- [!] BLOCKED\`) → tick the checkbox → next task without asking. -- **No Stop hook** — self-enforce: never end the session while \`.toh/plan.md\` has unchecked, unblocked tasks. -- **No model routing** — ignore haiku/sonnet/opus tiers in TOH docs. - -## Memory protocol (tiered) +## ${thai ? 'ข้อจำกัดของ Codex' : 'Codex constraints'} -- BEFORE work: read \`.toh/memory/active.md\` + \`summary.md\` (always); \`architecture.md\` + \`components.md\` for build tasks; \`changelog.md\` for debugging; \`decisions.md\` / \`agents-log.md\` only when referenced. -- AFTER work: always update \`active.md\`; update the others per relevance. Memory files are always in English. -- Close every stage per \`.toh/skills/engineer-harness/SKILL.md\` (Status / Result / Evidence / exactly 3 next actions). +${constraints} ${AGENTS_BLOCK_END}`; } -function generateAgentsMdTH(catalog) { - return `${AGENTS_BLOCK_START} -# 🎯 Toh Framework - -> **"Type Once, Have it all!"** — AI-Orchestration Driven Development -> "สั่งครั้งเดียว จบครบโดยไม่ต้องถาม" - -## Identity - -คุณคือ **Toh Framework Agent** ที่รันอยู่บน **Codex CLI** — ช่วย Solo Developer สร้าง SaaS คนเดียวจนจบ - -${renderCapabilitiesSection('codex')} - -## การใช้ TOH ใน Codex (native skills) - -เวิร์กโฟลว์ TOH ถูกติดตั้งเป็น **native Codex skills** ไว้ที่ \`.codex/skills/\` — นี่คือช่องทางหลัก: - -${renderSkillTable(catalog)} - -- เรียกตรงๆ ด้วย \`$\` (หรือพิมพ์ \`/skills\` เพื่อดูทั้งหมด) หรือแค่บรรยายงาน — Codex จะจับคู่ skill จาก description เอง -- แต่ละ skill อ่านเวิร์กโฟลว์จาก \`.toh/commands/\` และกฎประกอบจาก \`.toh/skills/\` - -### ข้อความ \`/toh-*\` แบบเดิม (compatibility) - -Codex ไม่มี slash command แบบกำหนดเอง ถ้าผู้ใช้พิมพ์ \`/toh-plan\` หรือ \`toh plan\` ให้ตีความว่าเป็นคำขอใช้ skill ที่ตรงกันใน \`.codex/skills/\` — เป็น compatibility behavior ไม่ใช่ native command - -## TOH runtime & state (\`.toh/\`) - -- \`.toh/plan.md\` + \`.toh/progress.md\` — แผนคือไฟล์; resume = task แรกที่ยังไม่ติ๊ก \`[ ]\` -- \`.toh/memory/\` — memory 7 ไฟล์ (protocol ด้านล่าง) -- \`.toh/skills/\` · \`.toh/commands/\` · \`.toh/capabilities.json\` - -## ข้อจำกัดของ Codex (เทียบ Claude Code) - -- **ไม่มี subagents/teams** — รัน THE TOH LOOP แบบ sequential ใน session นี้ (ดู \`.toh/skills/orchestration-protocol/SKILL.md\`): implement → รัน checkpoint ของ task → quote output จริง → แก้ถ้าแดง (สูงสุด 5 ครั้ง; แพ้ 3 ครั้งติด = \`- [!] BLOCKED\`) → ติ๊ก checkbox → ทำ task ถัดไปโดยไม่ต้องถาม -- **ไม่มี Stop hook** — บังคับตัวเอง: ห้ามจบ session ถ้า \`.toh/plan.md\` ยังมี task ที่ไม่ได้ติ๊กและไม่ blocked -- **ไม่มี model routing** — ข้าม tier haiku/sonnet/opus ในเอกสาร TOH - -## Memory protocol (tiered) - -- ก่อนทำงาน: อ่าน \`.toh/memory/active.md\` + \`summary.md\` เสมอ; \`architecture.md\` + \`components.md\` สำหรับงาน build; \`changelog.md\` สำหรับงาน debug; \`decisions.md\` / \`agents-log.md\` เฉพาะเมื่อถูกอ้างถึง -- หลังทำงาน: อัปเดต \`active.md\` เสมอ; ไฟล์อื่นตามความเกี่ยวข้อง — memory ทุกไฟล์เป็นภาษาอังกฤษ -- ปิดทุก stage ตาม \`.toh/skills/engineer-harness/SKILL.md\` (Status / Result / Evidence / next actions 3 ข้อ) +export function assertAgentsMdSize(content) { + const bytes = Buffer.byteLength(content, 'utf8'); + if (bytes > AGENTS_MAX_BYTES) { + throw new Error(`AGENTS.md is ${bytes} bytes; Codex limit is ${AGENTS_MAX_BYTES} bytes.`); + } +} -${AGENTS_BLOCK_END}`; +async function updateManifest(targetDir, changes) { + const manifest = await readManifest(targetDir); + await writeManifest(targetDir, { ...manifest, ...changes }); } -/** - * Insert or replace the TOH-managed block in AGENTS.md. All existing user - * content outside the markers is preserved; duplicate TOH blocks collapse - * into one, so repeated installs are idempotent by construction. - */ -export async function updateAgentsMd(targetDir, catalog, language = 'en') { - const block = language === 'th' ? generateAgentsMdTH(catalog) : generateAgentsMdEN(catalog); +export async function updateAgentsMd(targetDir, wrappers, language = 'en') { + const block = generateAgentsBlock(wrappers, language); const agentsPath = path.join(targetDir, 'AGENTS.md'); - - if (await fs.pathExists(agentsPath)) { - const existing = await fs.readFile(agentsPath, 'utf8'); - const stripped = existing.replace(AGENTS_BLOCK_RE, '').trimEnd(); - const next = stripped ? `${stripped}\n\n${block}\n` : `${block}\n`; - await fs.writeFile(agentsPath, next); - } else { - await fs.writeFile(agentsPath, `${block}\n`); - } + const existing = await fs.pathExists(agentsPath) ? await fs.readFile(agentsPath, 'utf8') : ''; + const stripped = existing.replace(AGENTS_BLOCK_RE, '').trimEnd(); + const next = stripped ? `${stripped}\n\n${block}\n` : `${block}\n`; + assertAgentsMdSize(next); + await fs.writeFile(agentsPath, next); + await updateManifest(targetDir, { agentsBlockSha256: sha256(block) }); return agentsPath; } -// ============================================================ -// .toh runtime (seed-if-absent — install.js is the primary seeder) -// ============================================================ +function codexConfigBlock() { + return `${CONFIG_BLOCK_START}\n# Keep Codex project instructions below its discovery ceiling.\nproject_doc_max_bytes = ${CODEX_PROJECT_DOC_MAX_BYTES}\n${CONFIG_BLOCK_END}`; +} + +export async function setupCodexConfig(targetDir) { + const configPath = path.join(targetDir, '.codex', 'config.toml'); + const existing = await fs.pathExists(configPath) ? await fs.readFile(configPath, 'utf8') : ''; + const withoutToh = existing.replace(CONFIG_BLOCK_RE, '').trimEnd(); + const hasUserQuota = /^\s*project_doc_max_bytes\s*=/m.test(withoutToh); + if (hasUserQuota) { + await updateManifest(targetDir, { configSha256: null }); + return configPath; + } + + const block = codexConfigBlock(); + const next = withoutToh ? `${withoutToh}\n\n${block}\n` : `${block}\n`; + await fs.ensureDir(path.dirname(configPath)); + await fs.writeFile(configPath, next); + await updateManifest(targetDir, { configSha256: sha256(next) }); + return configPath; +} -/** - * Guarantee the TOH runtime skeleton exists. Seeds only when absent so a - * reinstall never clobbers live memory/plan state. install.js already creates - * the full runtime before IDE handlers run; this makes setupCodex() safe to - * call standalone (tests, partial reinstalls). - */ export async function ensureTohRuntime(targetDir) { const tohDir = path.join(targetDir, '.toh'); const memoryDir = path.join(tohDir, 'memory'); @@ -338,97 +378,119 @@ export async function ensureTohRuntime(targetDir) { const today = new Date().toISOString().split('T')[0]; const seeds = { - 'active.md': `# 🔥 Active Task\n\n## Current Work\n[No active task - Waiting for user command]\n\n## Last Action\n[None]\n\n## Next Steps\n- Waiting for user command\n\n## Blockers\n[None]\n`, - 'summary.md': `# 📋 Project Summary\n\n## Project Info\n- **Name:** [Not specified]\n- **Type:** [Not specified]\n\n## Completed Features\n[None yet]\n\n## In Progress\n[None yet]\n`, + 'active.md': '# 🔥 Active Task\n\n## Current Work\n[No active task - Waiting for user command]\n\n## Last Action\n[None]\n\n## Next Steps\n- Waiting for user command\n\n## Blockers\n[None]\n', + 'summary.md': '# 📋 Project Summary\n\n## Project Info\n- **Name:** [Not specified]\n- **Type:** [Not specified]\n\n## Completed Features\n[None yet]\n\n## In Progress\n[None yet]\n', 'decisions.md': `# 🧠 Key Decisions\n\n## Architecture Decisions\n| Date | Decision | Reason |\n|------|----------|--------|\n| ${today} | Use Toh Framework v${VERSION} | AI-Orchestration Driven Development |\n`, 'changelog.md': `# 📝 Session Changelog\n\n## [Current Session] - ${today}\n\n### Changes Made\n| Agent | Action | File/Component |\n|-------|--------|----------------|\n| - | - | - |\n`, - 'agents-log.md': `# 🤖 Agents Activity Log\n\n## Recent Activity\n| Time | Agent | Task | Status | Files |\n|------|-------|------|--------|-------|\n| - | - | - | - | - |\n`, - 'architecture.md': `# 🏗️ Code Architecture\n\n## Directory Structure\n\`\`\`\n[Will be auto-generated when project starts]\n\`\`\`\n`, - 'components.md': `# 🧩 Component Registry\n\n## UI Components\n| Component | Path | Props | Used In |\n|-----------|------|-------|---------|\n| - | - | - | - |\n` + 'agents-log.md': '# 🤖 Agents Activity Log\n\n## Recent Activity\n| Time | Agent | Task | Status | Files |\n|------|-------|------|--------|-------|\n| - | - | - | - | - |\n', + 'architecture.md': '# 🏗️ Code Architecture\n\n## Directory Structure\n```\n[Will be auto-generated when project starts]\n```\n', + 'components.md': '# 🧩 Component Registry\n\n## UI Components\n| Component | Path | Props | Used In |\n|-----------|------|-------|---------|\n| - | - | - | - |\n' }; for (const [file, content] of Object.entries(seeds)) { - const p = path.join(memoryDir, file); - if (!(await fs.pathExists(p))) await fs.writeFile(p, content); + const filePath = path.join(memoryDir, file); + if (!(await fs.pathExists(filePath))) await fs.writeFile(filePath, content); } const planPath = path.join(tohDir, 'plan.md'); if (!(await fs.pathExists(planPath))) { - await fs.writeFile( - planPath, - `# Plan: (no active plan yet)\nStatus: draft\nCreated: ${today} by toh-framework installer\n\n> This file is THE TOH LOOP's backlog. Full schema + loop protocol:\n> \`.toh/skills/orchestration-protocol/SKILL.md\` (Section D).\n\nEmpty backlog — no stories yet. Run the \`toh-plan\` skill to draft a plan here, or\n\`toh-vibe\` to auto-generate a mini-plan and build it.\n` - ); + await fs.writeFile(planPath, `# Plan: (no active plan yet)\nStatus: draft\nCreated: ${today} by toh-framework installer\n\n> This file is THE TOH LOOP's backlog. Full schema + loop protocol:\n> \.toh/skills/orchestration-protocol/SKILL.md\n\nEmpty backlog — no stories yet. Run the \.toh-plan skill to draft a plan here, or\n\.toh-vibe to auto-generate a mini-plan and build it.\n`); } const progressPath = path.join(tohDir, 'progress.md'); if (!(await fs.pathExists(progressPath))) { - await fs.writeFile( - progressPath, - `# Progress Ledger\n\n> Append-only, one line per state change: \`queued → running → done/failed/blocked\`.\n> Format: \`YYYY-MM-DD HH:MM T00x — \`. Never rewrite history — append.\n` - ); + await fs.writeFile(progressPath, '# Progress Ledger\n\n> Append-only, one line per state change: `queued → running → done/failed/blocked`.\n> Format: `YYYY-MM-DD HH:MM T00x — `. Never rewrite history — append.\n'); } } -// ============================================================ -// Public API -// ============================================================ - -/** - * Install the Codex integration. Returns a short detail string for the - * install spinner. - */ export async function setupCodex(targetDir, srcDir, language = 'en') { await ensureTohRuntime(targetDir); - const catalog = await readCommandCatalog(srcDir); + const { wrappers } = await readWrapperCatalog(srcDir); + await setupCodexConfig(targetDir); const installed = await installCodexSkills(targetDir, srcDir); - await updateAgentsMd(targetDir, catalog, language); - return `.codex/skills/ (${installed.length} skills) + AGENTS.md`; + await updateAgentsMd(targetDir, wrappers, language); + return `${CODEX_SKILLS_DIR}/ (${installed.length}/${wrappers.length} wrappers) + AGENTS.md + .codex/config.toml`; } -/** - * Remove ONLY the TOH-managed Codex files: - * - .codex/skills/ dirs whose SKILL.md carries the TOH generator marker - * - the TOH-managed block in AGENTS.md (file deleted if it becomes empty) - * User skills, user AGENTS.md content, and .toh/ runtime state are preserved. - * Returns { removedSkills, agentsMd: 'updated'|'removed'|'absent' }. - */ -export async function uninstallCodex(targetDir) { - const result = { removedSkills: [], agentsMd: 'absent' }; - - const skillsRoot = path.join(targetDir, '.codex', 'skills'); - if (await fs.pathExists(skillsRoot)) { - for (const entry of await fs.readdir(skillsRoot, { withFileTypes: true })) { - if (!entry.isDirectory()) continue; - const dir = path.join(skillsRoot, entry.name); - const skillFile = path.join(dir, 'SKILL.md'); - if (!(await fs.pathExists(skillFile))) continue; - try { - const head = (await fs.readFile(skillFile, 'utf8')).slice(0, 4096); - if (head.includes(`generator: ${SKILL_GENERATOR}`)) { - await fs.remove(dir); - result.removedSkills.push(entry.name); - } - } catch { - // Leave unreadable entries untouched. - } - } - result.removedSkills.sort(); +async function makeBackup(targetDir, files) { + if (files.length === 0) return null; + const backupDir = path.join(targetDir, '.toh-backups', `codex-${Date.now()}`); + for (const relative of files) { + const source = path.join(targetDir, relative); + if (!(await fs.pathExists(source))) continue; + const destination = path.join(backupDir, relative); + await fs.ensureDir(path.dirname(destination)); + await fs.copy(source, destination); + } + return backupDir; +} + +export async function uninstallCodex(targetDir, options = {}) { + const { dryRun = false, backup = true } = options; + const manifest = await readManifest(targetDir); + const skillRemovals = []; + const backupFiles = []; + + for (const [relative, record] of Object.entries(manifest.files || {})) { + if (!relative.startsWith(`${CODEX_SKILLS_DIR}/`) || !relative.endsWith('/SKILL.md')) continue; + const filePath = path.join(targetDir, relative); + if (!(await fileMatchesHash(filePath, record.sha256))) continue; + skillRemovals.push({ relative, filePath, name: path.basename(path.dirname(filePath)) }); + backupFiles.push(relative); } + let agentsAction = 'absent'; const agentsPath = path.join(targetDir, 'AGENTS.md'); - if (await fs.pathExists(agentsPath)) { + if (await fs.pathExists(agentsPath) && manifest.agentsBlockSha256) { const existing = await fs.readFile(agentsPath, 'utf8'); - const stripped = existing.replace(AGENTS_BLOCK_RE, '').trim(); - if (stripped === existing.trim()) { - result.agentsMd = 'absent'; // no TOH block -> nothing to do - } else if (stripped) { - await fs.writeFile(agentsPath, `${stripped}\n`); - result.agentsMd = 'updated'; - } else { - await fs.remove(agentsPath); // block was the whole file -> file was ours - result.agentsMd = 'removed'; + const blockMatch = existing.match(/[\s\S]*?/); + if (blockMatch && sha256(blockMatch[0]) === manifest.agentsBlockSha256) { + agentsAction = existing.replace(AGENTS_BLOCK_RE, '').trim() ? 'updated' : 'removed'; + backupFiles.push('AGENTS.md'); } } + let configAction = 'absent'; + const configPath = path.join(targetDir, '.codex', 'config.toml'); + if (manifest.configSha256 && await fileMatchesHash(configPath, manifest.configSha256)) { + const existing = await fs.readFile(configPath, 'utf8'); + const stripped = existing.replace(CONFIG_BLOCK_RE, '').trim(); + configAction = stripped ? 'updated' : 'removed'; + backupFiles.push(path.relative(targetDir, configPath)); + } + + const result = { + removedSkills: skillRemovals.map((item) => item.name).sort(), + agentsMd: agentsAction, + config: configAction, + dryRun, + backupPath: null + }; + + if (dryRun) return result; + if (backup) result.backupPath = await makeBackup(targetDir, [...new Set(backupFiles)]); + + for (const item of skillRemovals) { + await fs.remove(item.filePath); + const parent = path.dirname(item.filePath); + if ((await fs.readdir(parent)).length === 0) await fs.remove(parent); + } + + if (agentsAction !== 'absent') { + const existing = await fs.readFile(agentsPath, 'utf8'); + const stripped = existing.replace(AGENTS_BLOCK_RE, '').trim(); + if (stripped) await fs.writeFile(agentsPath, `${stripped}\n`); + else await fs.remove(agentsPath); + } + + if (configAction !== 'absent') { + const existing = await fs.readFile(configPath, 'utf8'); + const stripped = existing.replace(CONFIG_BLOCK_RE, '').trim(); + if (stripped) await fs.writeFile(configPath, `${stripped}\n`); + else await fs.remove(configPath); + } + + const manifestPath = path.join(targetDir, MANIFEST_PATH); + if (await fs.pathExists(manifestPath)) await fs.remove(manifestPath); return result; -} +} \ No newline at end of file diff --git a/installer/install.js b/installer/install.js index 88cf35e..af3a011 100644 --- a/installer/install.js +++ b/installer/install.js @@ -224,6 +224,8 @@ async function promptConfiguration(defaults) { async function checkExistingInstall(targetDir) { const markers = [ join(targetDir, '.toh'), + join(targetDir, '.agents', 'skills'), + join(targetDir, '.codex', 'config.toml'), join(targetDir, '.claude', 'skills', 'vibe-orchestrator'), join(targetDir, '.cursor', 'rules', 'toh-framework.mdc') ]; @@ -247,7 +249,7 @@ async function cleanExistingInstall(targetDir) { } } - // Codex: remove TOH-managed .codex/skills + the AGENTS.md TOH block, + // Codex: remove TOH-managed .agents/skills + the AGENTS.md TOH block, // preserving any user skills and user AGENTS.md content. await uninstallCodex(targetDir); @@ -262,6 +264,7 @@ async function setupIDEWithSpinner(ideName, setupFn) { spinner.succeed(`${ideName} configured (${configFile})`); } catch (error) { spinner.fail(`Failed to configure ${ideName}: ${error.message}`); + throw error; } } @@ -270,7 +273,7 @@ function getIDEConfigFile(ideName) { 'Claude Code': 'created CLAUDE.md', 'Cursor': '.cursor/rules/*.mdc', 'Gemini CLI': '.gemini/GEMINI.md', - 'Codex CLI': '.codex/skills/ + AGENTS.md' + 'Codex CLI': '.agents/skills/ + AGENTS.md + .codex/config.toml' }; return configs[ideName] || 'configured'; } @@ -671,7 +674,7 @@ function printNextSteps(config) { // 13 green + 47 gray = 60 ; 11 green + 49 gray = 60 ; 18 green + 42 gray = 60 console.log(row(chalk.green(' $toh-vibe') + chalk.gray(' - Native skill: new project'.padEnd(47)))); console.log(row(chalk.green(' /skills') + chalk.gray(' - Browse all TOH skills'.padEnd(49)))); - console.log(row(chalk.green(' .codex/skills/') + chalk.gray(' - 14 native skills installed'.padEnd(42)))); + console.log(row(chalk.green(' .agents/skills/') + chalk.gray(' - 37 native wrappers installed'.padEnd(42)))); console.log(empty); } diff --git a/installer/status.js b/installer/status.js index bf9667c..4175f92 100644 --- a/installer/status.js +++ b/installer/status.js @@ -40,7 +40,8 @@ export async function status() { const projectPaths = [ { path: join(cwd, '.claude'), name: '.claude/' }, { path: join(cwd, '.cursor', 'rules'), name: '.cursor/rules/' }, - { path: join(cwd, '.codex', 'skills'), name: '.codex/skills/' }, + { path: join(cwd, '.agents', 'skills'), name: '.agents/skills/' }, + { path: join(cwd, '.codex', 'config.toml'), name: '.codex/config.toml' }, { path: join(cwd, '.toh'), name: '.toh/' }, { path: join(cwd, 'CLAUDE.md'), name: 'CLAUDE.md' }, { path: join(cwd, 'AGENTS.md'), name: 'AGENTS.md' }, diff --git a/installer/uninstall.js b/installer/uninstall.js index 2e7800c..6ca164a 100644 --- a/installer/uninstall.js +++ b/installer/uninstall.js @@ -4,13 +4,13 @@ * Removes TOH-managed files from a target project. * * Scoping rules: - * --ide codex -> only Codex-owned TOH files (.codex/skills/toh-* managed + * --ide codex -> only Codex-owned TOH files (.agents/skills managed * skills + the AGENTS.md TOH block). `.toh/` is shared * runtime state and stays — other IDEs may still use it. * (no --ide) -> full removal: every IDE surface the installer owns, * plus `.toh/`. * - * User content is never deleted: user skills under .codex/skills, user text + * User content is never deleted: user skills under .agents/skills, user text * in AGENTS.md outside the TOH markers, and files we cannot classify are * all left untouched. */ @@ -23,6 +23,8 @@ import { uninstallCodex } from './ide-handlers/codex.js'; export async function uninstall(options) { const targetDir = options.target || process.cwd(); + const dryRun = options.dryRun === true; + const backup = options.backup !== false; const ides = options.ide ? options.ide.split(',').map((i) => i.trim().toLowerCase()) : null; // null = full uninstall @@ -46,17 +48,19 @@ export async function uninstall(options) { // ---- Codex teardown (per-IDE and full uninstall both do this) ---- const spinner = ora('Removing Codex integration...').start(); try { - const { removedSkills, agentsMd } = await uninstallCodex(targetDir); + const { removedSkills, agentsMd, config, backupPath } = await uninstallCodex(targetDir, { dryRun, backup }); const parts = []; - parts.push(removedSkills.length ? `${removedSkills.length} skill(s) removed` : 'no TOH skills found'); - if (agentsMd === 'updated') parts.push('AGENTS.md block removed'); - if (agentsMd === 'removed') parts.push('AGENTS.md removed (was TOH-only)'); + parts.push(removedSkills.length ? `${removedSkills.length} skill(s) ${dryRun ? 'would be removed' : 'removed'}` : 'no TOH skills found'); + if (agentsMd === 'updated') parts.push(`AGENTS.md block ${dryRun ? 'would be removed' : 'removed'}`); + if (agentsMd === 'removed') parts.push(`AGENTS.md ${dryRun ? 'would be removed' : 'removed (was TOH-only)'}`); + if (config === 'updated' || config === 'removed') parts.push(`Codex config ${dryRun ? 'would be updated' : 'updated'}`); + if (backupPath) parts.push(`backup: ${backupPath}`); spinner.succeed(`Codex integration removed (${parts.join(', ')})`); } catch (error) { spinner.fail(`Failed to remove Codex integration: ${error.message}`); } - if (!ides) { + if (!ides && !dryRun) { // ---- Full uninstall: remaining TOH-owned paths ---- const fullPaths = [ join(targetDir, '.toh'), @@ -86,5 +90,9 @@ export async function uninstall(options) { ); } + if (!ides && dryRun) { + console.log(chalk.yellow(' Dry run: shared TOH runtime and IDE directories were left untouched.')); + } + console.log(chalk.green('\n✅ Uninstall complete.\n')); } diff --git a/src/agents/README.md b/src/agents/README.md index 98c2b61..825e990 100644 --- a/src/agents/README.md +++ b/src/agents/README.md @@ -16,7 +16,7 @@ src/agents/*.md ← single source (superset frontmatter + canonical ├── Claude Code → copy as-is → .claude/agents/*.md │ (uses native name / description / tools / model) ├── Cursor → strip frontmatter → bundle into a .mdc rules file - ├── Codex → strip frontmatter → embed body in AGENTS.md + ├── Codex → generate thin wrappers in .agents/skills/; keep runtime body in .toh/ └── Gemini / Antigravity → convert frontmatter to each IDE's format ``` @@ -78,7 +78,7 @@ The same source produces IDE-appropriate output at install time: |-----|----------------|------------------------| | Claude Code | `.claude/agents/*.md` | Copied as-is (native `name`/`description`/`tools`/`model`) | | Cursor | `.cursor/rules/…` | Frontmatter stripped → bundled as rules | -| Codex CLI | `AGENTS.md` | Frontmatter stripped → body embedded | +| Codex CLI | `.agents/skills/*.md` + `AGENTS.md` | 23 supporting-skill + 14 command wrappers; runtime body stays in `.toh/` | | Gemini / Antigravity | `.toh/agents/*.md` | Frontmatter converted per IDE | ``` diff --git a/tests/codex.test.js b/tests/codex.test.js index 5982209..9eaddd6 100644 --- a/tests/codex.test.js +++ b/tests/codex.test.js @@ -2,13 +2,14 @@ * Codex integration tests (node:test). * * Covers: - * - fresh installation layout (.toh/, .codex/skills/, AGENTS.md) + * - fresh installation layout (.toh/, .agents/skills/, AGENTS.md) * - preservation of existing AGENTS.md content * - idempotency + deterministic output across reinstalls * - preservation of unrelated user Codex skills * - removal of stale TOH-managed skills * - uninstall (TOH files out, user files stay) * - SKILL.md validity (frontmatter, non-empty body, resolving references) + * - AGENTS.md size guard and Codex project-doc quota * * Run: npm test */ @@ -19,6 +20,7 @@ process.env.TOH_QUIET = '1'; import { test, before, after } from 'node:test'; import assert from 'node:assert/strict'; +import { createHash } from 'node:crypto'; import fs from 'fs-extra'; import os from 'os'; import path from 'path'; @@ -27,17 +29,21 @@ import yaml from 'js-yaml'; import { install } from '../installer/install.js'; import { + AGENTS_MAX_BYTES, + CODEX_SKILLS_DIR, + assertAgentsMdSize, setupCodex, uninstallCodex, installCodexSkills, - readCommandCatalog + readCommandCatalog, + readSupportingSkillCatalog } from '../installer/ide-handlers/codex.js'; const __filename = fileURLToPath(import.meta.url); const REPO_ROOT = path.join(path.dirname(__filename), '..'); const SRC_DIR = path.join(REPO_ROOT, 'src'); -const EXPECTED_SKILLS = [ +const EXPECTED_COMMANDS = [ 'toh', 'toh-connect', 'toh-design', @@ -87,7 +93,7 @@ function parseSkillFrontmatter(raw) { // ---------------------------------------------------------------- tests -test('fresh install creates .toh/, .codex/skills/ and AGENTS.md', async () => { +test('fresh install creates .toh/, .agents/skills/, config.toml and AGENTS.md', async () => { const dir = await makeTmpProject(); try { await quickInstallCodex(dir); @@ -97,10 +103,16 @@ test('fresh install creates .toh/, .codex/skills/ and AGENTS.md', async () => { assert.ok(await fs.pathExists(path.join(dir, '.toh', 'memory', 'active.md')), 'memory seeded'); assert.ok(await fs.pathExists(path.join(dir, '.toh', 'skills', 'orchestration-protocol', 'SKILL.md')), '.toh skills installed'); assert.ok(await fs.pathExists(path.join(dir, 'AGENTS.md')), 'AGENTS.md exists'); - - for (const skill of EXPECTED_SKILLS) { + assert.ok(await fs.pathExists(path.join(dir, '.codex', 'config.toml')), 'Codex config exists'); + assert.match(await fs.readFile(path.join(dir, '.codex', 'config.toml'), 'utf8'), /project_doc_max_bytes\s*=\s*65536/); + assert.ok(!(await fs.pathExists(path.join(dir, '.codex', 'skills'))), 'legacy .codex/skills is not used'); + + const supporting = await readSupportingSkillCatalog(SRC_DIR); + assert.equal(supporting.length, 23, 'all 23 supporting skills are catalogued'); + assert.equal((await fs.readdir(path.join(dir, CODEX_SKILLS_DIR))).length, 37, '14 commands + 23 skills are wrapped'); + for (const skill of [...supporting.map((entry) => entry.skillName), ...EXPECTED_COMMANDS]) { assert.ok( - await fs.pathExists(path.join(dir, '.codex', 'skills', skill, 'SKILL.md')), + await fs.pathExists(path.join(dir, CODEX_SKILLS_DIR, skill, 'SKILL.md')), `native skill installed: ${skill}` ); } @@ -159,7 +171,7 @@ test('reinstall is idempotent and deterministic', async () => { test('unrelated user Codex skills are never touched', async () => { const dir = await makeTmpProject(); try { - const userSkillDir = path.join(dir, '.codex', 'skills', 'my-company-skill'); + const userSkillDir = path.join(dir, CODEX_SKILLS_DIR, 'my-company-skill'); await fs.ensureDir(userSkillDir); const userSkill = '---\nname: my-company-skill\ndescription: mine\n---\n\nUser-owned.\n'; await fs.writeFile(path.join(userSkillDir, 'SKILL.md'), userSkill); @@ -172,8 +184,8 @@ test('unrelated user Codex skills are never touched', async () => { 'user skill content unchanged' ); - // Even a user-owned skill with a toh- name survives (no generator marker). - const userTohDir = path.join(dir, '.codex', 'skills', 'toh-custom'); + // Even a user-owned skill with a toh- name survives (no ownership hash). + const userTohDir = path.join(dir, CODEX_SKILLS_DIR, 'toh-custom'); await fs.ensureDir(userTohDir); const userToh = '---\nname: toh-custom\ndescription: user owned\n---\n\nMine.\n'; await fs.writeFile(path.join(userTohDir, 'SKILL.md'), userToh); @@ -189,18 +201,27 @@ test('stale TOH-managed skills are removed on reinstall', async () => { try { await quickInstallCodex(dir); - // Simulate a skill generated by an older TOH version (has the marker). - const staleDir = path.join(dir, '.codex', 'skills', 'toh-legacy'); + // Simulate a skill generated by an older TOH version with a manifest entry. + const staleDir = path.join(dir, CODEX_SKILLS_DIR, 'toh-legacy'); await fs.ensureDir(staleDir); + const staleContent = '---\nname: toh-legacy\ndescription: old\nmetadata:\n generator: toh-framework\n---\n\nOld.\n'; await fs.writeFile( path.join(staleDir, 'SKILL.md'), - '---\nname: toh-legacy\ndescription: old\nmetadata:\n generator: toh-framework\n---\n\nOld.\n' + staleContent ); + const manifestPath = path.join(dir, '.codex', 'toh-framework.json'); + const manifest = await fs.readJson(manifestPath); + manifest.files['.agents/skills/toh-legacy/SKILL.md'] = { + sha256: createHash('sha256').update(staleContent).digest('hex'), + kind: 'command', + source: '.toh/commands/toh-legacy.md' + }; + await fs.writeJson(manifestPath, manifest, { spaces: 2 }); await setupCodex(dir, SRC_DIR, 'en'); assert.ok(!(await fs.pathExists(staleDir)), 'stale TOH-managed skill removed'); - assert.ok(await fs.pathExists(path.join(dir, '.codex', 'skills', 'toh-plan', 'SKILL.md')), 'current skills intact'); + assert.ok(await fs.pathExists(path.join(dir, CODEX_SKILLS_DIR, 'toh-plan', 'SKILL.md')), 'current skills intact'); } finally { await fs.remove(dir); } @@ -211,7 +232,7 @@ test('uninstall removes TOH Codex files and keeps user files + .toh state', asyn try { // Pre-existing user content await fs.writeFile(path.join(dir, 'AGENTS.md'), '# My Project\n\nKeep this text.\n'); - const userSkillDir = path.join(dir, '.codex', 'skills', 'my-company-skill'); + const userSkillDir = path.join(dir, CODEX_SKILLS_DIR, 'my-company-skill'); await fs.ensureDir(userSkillDir); await fs.writeFile(path.join(userSkillDir, 'SKILL.md'), '---\nname: my-company-skill\ndescription: mine\n---\n'); @@ -220,14 +241,16 @@ test('uninstall removes TOH Codex files and keeps user files + .toh state', asyn // Pretend the loop ran: live state that must survive a codex uninstall. await fs.writeFile(path.join(dir, '.toh', 'plan.md'), '# Plan: real work\n\n- [ ] T001 [P] ui-builder — thing in app/page.tsx\n'); - const { removedSkills, agentsMd } = await uninstallCodex(dir); + const { removedSkills, agentsMd, config, backupPath } = await uninstallCodex(dir); - assert.equal(removedSkills.length, EXPECTED_SKILLS.length, 'all TOH skills removed'); - for (const skill of EXPECTED_SKILLS) { - assert.ok(!(await fs.pathExists(path.join(dir, '.codex', 'skills', skill))), `removed ${skill}`); + assert.equal(removedSkills.length, 37, 'all TOH wrappers removed'); + for (const skill of EXPECTED_COMMANDS) { + assert.ok(!(await fs.pathExists(path.join(dir, CODEX_SKILLS_DIR, skill))), `removed ${skill}`); } assert.ok(await fs.pathExists(path.join(userSkillDir, 'SKILL.md')), 'user skill remains'); - assert.ok(await fs.pathExists(path.join(dir, '.codex', 'skills')), '.codex/skills dir kept'); + assert.ok(await fs.pathExists(path.join(dir, CODEX_SKILLS_DIR)), '.agents/skills dir kept'); + assert.equal(config, 'removed'); + assert.ok(backupPath && await fs.pathExists(backupPath), 'uninstall creates a backup'); assert.equal(agentsMd, 'updated'); const agents = await fs.readFile(path.join(dir, 'AGENTS.md'), 'utf8'); @@ -249,6 +272,7 @@ test('uninstall deletes AGENTS.md only when it was TOH-only', async () => { const { agentsMd } = await uninstallCodex(dir); assert.equal(agentsMd, 'removed'); assert.ok(!(await fs.pathExists(path.join(dir, 'AGENTS.md'))), 'TOH-only AGENTS.md removed'); + assert.ok(!(await fs.pathExists(path.join(dir, '.codex', 'config.toml'))), 'TOH-only config removed'); } finally { await fs.remove(dir); } @@ -259,9 +283,9 @@ test('every generated SKILL.md is valid and its references resolve', async () => try { await quickInstallCodex(dir); - const skillsRoot = path.join(dir, '.codex', 'skills'); + const skillsRoot = path.join(dir, CODEX_SKILLS_DIR); const entries = (await fs.readdir(skillsRoot, { withFileTypes: true })).filter((e) => e.isDirectory()); - assert.equal(entries.length, EXPECTED_SKILLS.length, 'exactly the TOH skills exist'); + assert.equal(entries.length, 37, 'exactly 37 TOH wrappers exist'); const NAME_RE = /^[a-z0-9-]{1,64}$/; for (const entry of entries) { @@ -273,9 +297,13 @@ test('every generated SKILL.md is valid and its references resolve', async () => assert.ok(typeof fm.description === 'string' && fm.description.length > 0, 'description present'); assert.ok(fm.description.length <= 1024, 'description within Codex limit'); assert.equal(fm.metadata?.generator, 'toh-framework', 'generator marker present'); + assert.ok(['command', 'skill'].includes(fm.metadata?.kind), 'wrapper kind present'); assert.ok(body.trim().length > 0, 'non-empty instructions'); // Every `.toh/...` reference in the body must resolve to a real file. + + const agents = await fs.readFile(path.join(dir, 'AGENTS.md'), 'utf8'); + assert.ok(Buffer.byteLength(agents, 'utf8') <= AGENTS_MAX_BYTES, 'AGENTS.md is below Codex limit'); const refs = raw.match(/`(\.toh\/[^`]+)`/g) || []; assert.ok(refs.length > 0, `${entry.name}: references .toh files`); for (const ref of refs) { @@ -295,7 +323,7 @@ test('skills reference the supporting .toh/skills from command frontmatter', asy const catalog = await readCommandCatalog(SRC_DIR); for (const entry of catalog) { const raw = await fs.readFile( - path.join(dir, '.codex', 'skills', entry.skillName, 'SKILL.md'), + path.join(dir, CODEX_SKILLS_DIR, entry.skillName, 'SKILL.md'), 'utf8' ); assert.ok(raw.includes(`.toh/commands/${entry.file}`), `${entry.skillName}: references its command file`); @@ -312,6 +340,62 @@ test('skills reference the supporting .toh/skills from command frontmatter', asy } }); +test('modified TOH files are preserved because ownership hashes no longer match', async () => { + const dir = await makeTmpProject(); + try { + await quickInstallCodex(dir); + const file = path.join(dir, CODEX_SKILLS_DIR, 'toh-plan', 'SKILL.md'); + const modified = `${await fs.readFile(file, 'utf8')}\nUser customization.\n`; + await fs.writeFile(file, modified); + + await quickInstallCodex(dir); + assert.equal(await fs.readFile(file, 'utf8'), modified, 'reinstall does not overwrite modified skill'); + + const result = await uninstallCodex(dir, { backup: false }); + assert.ok(!result.removedSkills.includes('toh-plan'), 'modified skill is not removed'); + assert.equal(await fs.readFile(file, 'utf8'), modified, 'uninstall keeps modified skill'); + } finally { + await fs.remove(dir); + } +}); + +test('AGENTS.md size assertion rejects files above the Codex ceiling', () => { + assert.throws( + () => assertAgentsMdSize('x'.repeat(AGENTS_MAX_BYTES + 1)), + /Codex limit/i + ); +}); + +test('install fails when the final AGENTS.md exceeds the Codex ceiling', async () => { + const dir = await makeTmpProject(); + try { + await fs.writeFile(path.join(dir, 'AGENTS.md'), 'x'.repeat(AGENTS_MAX_BYTES)); + await assert.rejects( + () => quickInstallCodex(dir), + /AGENTS\.md is .*Codex limit/i + ); + } finally { + await fs.remove(dir); + } +}); + +test('uninstall dry-run reports owned files without changing the project', async () => { + const dir = await makeTmpProject(); + try { + await quickInstallCodex(dir); + const before = await snapshotTree(dir); + const result = await uninstallCodex(dir, { dryRun: true }); + + assert.equal(result.dryRun, true); + assert.equal(result.removedSkills.length, 37); + assert.equal(result.agentsMd, 'removed'); + assert.equal(result.config, 'removed'); + assert.deepEqual([...before.keys()].sort(), [...(await snapshotTree(dir)).keys()].sort(), 'dry-run keeps file set'); + } finally { + await fs.remove(dir); + } +}); + test('catalog parsing fails with an actionable error on a broken package', async () => { const dir = await makeTmpProject(); try { From 260420adce959a3b51242d62b9186a5996cdd373 Mon Sep 17 00:00:00 2001 From: Patipat Chewprecha Date: Mon, 31 Aug 2026 09:54:05 +0700 Subject: [PATCH 3/4] feat: Enhance Codex integration with native agents and improved capabilities - Updated capability profiles to include 'unknown' subagent type for better handling of unknown IDEs. - Introduced native agents for Codex, allowing for improved task delegation and parallel execution. - Modified installer to generate native agent files in `.codex/agents/` and command skills in `.agents/skills/`. - Enhanced uninstall process to correctly manage native agents and preserve user modifications. - Updated tests to validate new agent generation, capabilities, and ensure existing user configurations remain intact. - Added support for new TOML parsing and configuration management for Codex. - Improved documentation to reflect changes in agent handling and capabilities. --- CHANGELOG.md | 14 +- CLAUDE.md | 27 +- README.md | 29 +- docs/README-TH.md | 28 +- installer/ide-handlers/codex.js | 304 ++++++++++++++++++--- installer/ide-handlers/shared.js | 33 ++- installer/install.js | 7 +- installer/uninstall.js | 11 +- package-lock.json | 15 +- package.json | 5 +- src/agents/README.md | 7 +- src/agents/backend-connector.md | 1 + src/agents/design-reviewer.md | 1 + src/agents/dev-builder.md | 1 + src/agents/plan-orchestrator.md | 1 + src/agents/platform-adapter.md | 1 + src/agents/root-cause-debugger.md | 1 + src/agents/test-runner.md | 1 + src/agents/ui-builder.md | 1 + src/skills/orchestration-protocol/SKILL.md | 11 +- src/skills/smart-routing/SKILL.md | 3 +- tests/codex.test.js | 186 ++++++++++++- 22 files changed, 571 insertions(+), 117 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd33898..173d929 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,24 +10,24 @@ The Codex integration no longer simulates `/toh-*` slash commands through a gian #### Changed -- **Codex handler rewritten** (`installer/ide-handlers/codex.js`) - now installs one thin native skill per TOH command (14 skills), each a wrapper that points at the real workflow in `.toh/commands/*.md` and the supporting skills in `.toh/skills/` — no duplicated workflow content. Codex constraints (no subagents, no Stop hook, no model routing) are stated explicitly with sequential fallbacks. -- **`AGENTS.md` block slimmed** - the managed `` block now carries only project-level rules: identity, capabilities, the native-skills table, legacy `/toh-*` compatibility note, `.toh` runtime map, and the tiered memory protocol. The ~800-line embedded copy of every agent body is gone (Codex has no subagents to run them). +- **Codex handler rewritten** (`installer/ide-handlers/codex.js`) - installs 14 thin native workflow skills that point at `.toh/commands/*.md`, plus eight native custom agents under `.codex/agents/*.toml`. Native agents carry Codex model, reasoning, developer instructions, and read-only sandbox boundaries translated from the canonical source. +- **`AGENTS.md` block slimmed** - the managed `` block now carries only project-level rules: identity, capabilities, the workflow-skills table, native-agent pointer, legacy `/toh-*` compatibility note, `.toh` runtime map, and the parent-owned checkpoint contract. - **Codex install output** - the success box now points at `$toh-vibe` / `/skills` and `.agents/skills/` instead of implying `/toh-*` is registered. - **Reinstall safety** - `.toh/memory/*.md` are now seeded only if absent (previously every reinstall overwrote them, contradicting the README's "without deleting your existing memory" promise); `plan.md` / `progress.md` were already protected. - **`--quick` reinstalls are non-interactive** - an existing install under `--quick` now defaults to Quick Update instead of prompting (interactive behavior unchanged). #### Added -- **Native Codex skills** - 23 supporting-skill wrappers + 14 command wrappers under `.agents/skills/`, generated from `src/skills/` and `src/commands/*.md` with a `metadata.generator: toh-framework` marker. +- **Native Codex skills and agents** - 14 command wrappers under `.agents/skills/`, generated from `src/commands/*.md` with a `metadata.generator: toh-framework` marker; 23 supporting skills remain internal under `.toh/skills/`; eight custom agents are generated from `src/agents/*.md`. - **Stale-skill cleanup** - reinstall removes previously TOH-generated skills that no longer exist, identified by the generator marker; user skills (even user skills named `toh-*`) are never touched. -- **`toh uninstall`** - new CLI command. `--ide codex` removes only TOH-managed Codex files (skills + AGENTS.md block, user content preserved); without `--ide` it also removes `.toh` and the other TOH-owned IDE resource dirs. The installer's Fresh Install cleanup uses the same Codex teardown. +- **`toh uninstall`** - new CLI command. `--ide codex` removes only TOH-managed Codex files (workflow skills + native agents + AGENTS.md block, user content preserved); without `--ide` it also removes `.toh` and the other TOH-owned IDE resource dirs. The installer's Fresh Install cleanup uses the same Codex teardown. - **`toh status`** - now reports `.agents/skills/`, `.codex/config.toml`, and `AGENTS.md`. -- **Test suite** - `tests/codex.test.js` (node:test, in-band runner via `tests/run.js`) covers fresh install, AGENTS.md preservation, idempotency/determinism, user-skill preservation, stale-skill removal, uninstall scoping, and SKILL.md validity with resolving references. Runs in CI (`npm test`) on Node 18 + 22. +- **Test suite** - `tests/codex.test.js` (node:test, in-band runner via `tests/run.js`) covers fresh install, native TOML agents, model/reasoning routing, read-only sandboxing, AGENTS.md preservation, idempotency/determinism, user/ZCode-file preservation, stale-skill removal, uninstall scoping, and SKILL.md validity. Runs in CI (`npm test`) on Node 18 + 22. #### Technical -- Agent count (**8**), skill count (**23**), command count (**14**) unchanged — Codex skills are generated wrappers, not new content. -- Synced IDE surfaces: Codex (`.agents/skills/` + `.codex/config.toml` + `AGENTS.md` block), README.md, docs/README-TH.md, installer output, `toh status`. +- Agent count (**8**), skill count (**23**), command count (**14**) unchanged — Codex agents and workflow skills are generated native surfaces, not new source content. +- Synced IDE surfaces: Codex CLI (`.codex/agents/` + `.agents/skills/` + `.codex/config.toml` + `AGENTS.md` block), README.md, docs/README-TH.md, installer output, `toh status`. - Version bumped from **2.0.0** to **2.1.0**. --- diff --git a/CLAUDE.md b/CLAUDE.md index 7d1b2cc..fa99d6b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,7 +5,7 @@ Guide for a Claude Code agent developing the framework itself. Repo-only: not in ## What this is -Toh Framework ("Type Once, Have it all") is the npm package `toh-framework` (v2.0.0, MIT, ESM, +Toh Framework ("Type Once, Have it all") is the npm package `toh-framework` (v2.1.0, MIT, ESM, Node >= 18, no build step) that installs an AI-orchestration development system into 5 IDEs: Claude Code, Cursor, Gemini CLI, Google Antigravity, and Codex CLI. @@ -54,15 +54,16 @@ then 4 handlers in `installer/ide-handlers/` (plus shared.js utilities) cover th - gemini-cli.js → BOTH Gemini CLI (.gemini/: TOML commands from src/gemini-commands/, skills, GEMINI.md) and Antigravity (.agent/workflows/ from src/antigravity-workflows/); selecting gemini auto-adds antigravity. There is no antigravity.js. -- codex.js → NATIVE Codex skills: one thin wrapper per supporting skill and command at - .agents/skills//SKILL.md (generated from src/skills/ and src/commands/; each wrapper - points at .toh/commands/*.md or .toh/skills/* and states Codex constraints — no subagents, - no Stop hook, sequential TOH LOOP) + a CONCISE managed AGENTS.md block - (TOH-FRAMEWORK-START/END: identity, capabilities, skills table, legacy `/toh-*` compat - note, memory protocol). Never embed agent bodies in AGENTS.md again (pre-v2.1 behavior — - Codex has no subagents to run them). Exports uninstallCodex() (removes only - generator-marked skills + the managed block); install.js cleanExistingInstall and the - `toh uninstall` CLI command both use it. `.toh/` runtime is seeded only-if-absent. +- codex.js → NATIVE Codex workflows: one thin command wrapper per user-facing command at + .agents/skills//SKILL.md (generated from src/commands/; each wrapper points at + .toh/commands/*.md and internal skills stay in .toh/skills/) + native custom agents at + .codex/agents/.toml (generated from src/agents/, with centralized model/reasoning + routing and read-only sandbox mapping) + a CONCISE managed AGENTS.md block + (TOH-FRAMEWORK-START/END: identity, capabilities, workflow table, native-agent pointer, + delegation/checkpoint contract, legacy `/toh-*` compat note, memory protocol). Never + embed agent bodies in AGENTS.md again. Exports uninstallCodex() (removes only + generator-marked skills/agents + the managed block); install.js cleanExistingInstall and + the `toh uninstall` CLI command both use it. `.toh/` runtime is seeded only-if-absent. Per-IDE command divergence lives in ONE markdown source via `` (kept only for Claude Code) / `` (kept for everyone else) blocks, resolved by shared.js @@ -73,9 +74,9 @@ loop — that is why claude-code.js transforms from package src/commands, not .t - `src/agents/` — exactly 8 agent .md files + README.md, no subdirectories (src/agents/subagents/ was deleted in v2.0.0, commit c33dcf3 — never reference it). Superset frontmatter: name, - "Delegate when:" description, narrow per-agent tools allowlist, model, skills, triggers, - optional memory/isolation/maxTurns; keep filename == frontmatter name. Model tiers are - deliberate cost routing: opus = plan-orchestrator (THE BRAIN) + design-reviewer; sonnet = + "Delegate when:" description, narrow per-agent tools allowlist, model, modelIntent, skills, + triggers, optional memory/isolation/maxTurns; keep filename == frontmatter name. Model tiers + are deliberate Claude cost routing: opus = plan-orchestrator (THE BRAIN) + design-reviewer; sonnet = ui-builder, dev-builder, backend-connector, platform-adapter, root-cause-debugger (read-only Read/Grep/Glob/Bash — proves root cause, never edits); haiku = test-runner (Playwright auto-fix, maxTurns 30). diff --git a/README.md b/README.md index e834c09..56539e3 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ | 📝 **Cursor** | ✅ Full Support | @ file references | | 🌌 **Google Antigravity** | ✅ Full Support | Gemini integration | | 💎 **Gemini CLI** | ✅ Full Support | Context files auto-loaded | -| 🤖 **Codex CLI** | ✅ Supported | OpenAI agents | +| 🤖 **Codex CLI** | ✅ Supported | Native skills and project agents | ## 💡 Why Toh? @@ -181,15 +181,16 @@ gemini ### Codex CLI -Codex has no custom slash commands, so TOH installs **native Codex skills** -under `.agents/skills/` — 23 supporting-skill wrappers plus 14 workflow -wrappers (`toh-vibe`, `toh-plan`, `toh-ui`, `toh-dev`, `toh-design`, -`toh-test`, `toh-connect`, `toh-line`, `toh-mobile`, `toh-fix`, `toh-ship`, -`toh-protect`, `toh-help`, `toh`). Codex project-doc quota is configured in -`.codex/config.toml`. +Codex has no custom slash commands, so TOH installs **native Codex workflow +skills** under `.agents/skills/` — 14 top-level `$toh-*` workflows. The 23 +supporting skills stay in `.toh/skills/` so the global skill list remains focused. +TOH also generates eight project-scoped native agents under `.codex/agents/*.toml`. +Codex CLI uses native project files, including native model and reasoning +routing for each agent. `.codex/config.toml` enables multi-agent +execution when the project does not already define its own `[features]` table. ```bash -# Open the project root in Codex +# Open the project root in Codex CLI codex # Invoke a TOH skill explicitly ($ + skill name), or browse with /skills @@ -201,13 +202,15 @@ $toh-plan build a booking app with payments ``` TOH stores framework state under `.toh/` (`plan.md`, `progress.md`, -`memory/`) and project-level rules in the managed block of `AGENTS.md`. -For compatibility, typing `/toh-vibe ...` as plain text is interpreted (via -`AGENTS.md`) as a request for the matching skill — but it is **not** a native -Codex slash command. +`memory/`), native workflow discovery under `.agents/skills/`, and native agent +definitions under `.codex/agents/`. Project-level rules live in the managed block +of `AGENTS.md`. For compatibility, typing `/toh-vibe ...` as plain text is +interpreted (via `AGENTS.md`) as a request for the matching skill — but it is +**not** a native Codex slash command. TOH does not read or modify ZCode files or +global Codex configuration. Uninstall (removes only TOH-managed Codex files; your own skills and -`AGENTS.md` text are kept): +agents, ZCode files, and `AGENTS.md` text are kept): ```bash npx toh-framework uninstall --ide codex diff --git a/docs/README-TH.md b/docs/README-TH.md index dd0bbfe..db27c41 100644 --- a/docs/README-TH.md +++ b/docs/README-TH.md @@ -22,7 +22,7 @@ | 📝 **Cursor** | ✅ รองรับเต็ม | @ file references | | 🌌 **Google Antigravity** | ✅ รองรับเต็ม | Gemini integration | | 💎 **Gemini CLI** | ✅ รองรับเต็ม | Context files auto-loaded | -| 🤖 **Codex CLI** | ✅ รองรับ | OpenAI agents | +| 🤖 **Codex CLI** | ✅ รองรับ | Native skills และ project agents | ## 💡 ทำไมต้อง Toh? @@ -165,15 +165,15 @@ gemini ### Codex CLI Codex ไม่มี slash command แบบกำหนดเอง ดังนั้น TOH จะติดตั้ง **native Codex -skills** ไว้ที่ `.agents/skills/` — wrapper ของ supporting skill 23 ตัวและ -workflow 14 ตัว -(`toh-vibe`, `toh-plan`, `toh-ui`, `toh-dev`, `toh-design`, `toh-test`, -`toh-connect`, `toh-line`, `toh-mobile`, `toh-fix`, `toh-ship`, -`toh-protect`, `toh-help`, `toh`) โดย quota ของเอกสารโปรเจคอยู่ใน -`.codex/config.toml` +workflow skills** ไว้ที่ `.agents/skills/` — workflow ระดับบน 14 ตัวในรูปแบบ +`$toh-*` ส่วน supporting skills 23 ตัวจะอยู่ใน `.toh/skills/` เพื่อให้รายการ +skill หลักกระชับ นอกจากนี้ TOH สร้าง native agents แบบ project-scoped 8 ตัวไว้ที่ +`.codex/agents/*.toml` โดย Codex CLI ใช้ไฟล์โปรเจคชุดเดียวกัน รวมถึงการกำหนด +model และ reasoning ของแต่ละ agent โดย `.codex/config.toml` จะเปิด +multi-agent ให้เมื่อโปรเจคยังไม่มี `[features]` ของตัวเอง ```bash -# เปิดโฟลเดอร์โปรเจคใน Codex +# เปิดโฟลเดอร์โปรเจคใน Codex CLI codex # เรียก skill ตรงๆ ด้วย $ + ชื่อ skill (หรือพิมพ์ /skills เพื่อดูทั้งหมด) @@ -185,13 +185,15 @@ $toh-plan สร้างแอปจองห้องพร้อมชำร ``` TOH เก็บ state ของ framework ไว้ที่ `.toh/` (`plan.md`, `progress.md`, -`memory/`) และกฎระดับโปรเจคไว้ใน block ที่ TOH จัดการของ `AGENTS.md` -เพื่อความเข้ากันได้แบบเดิม ถ้าพิมพ์ `/toh-vibe ...` เป็นข้อความธรรมดา -ระบบจะตีความ (ผ่าน `AGENTS.md`) ว่าเป็นการเรียก skill ที่ตรงกัน — -แต่มัน **ไม่ใช่** native slash command ของ Codex +`memory/`), workflow discovery ไว้ที่ `.agents/skills/` และ native agent +definitions ไว้ที่ `.codex/agents/` กฎระดับโปรเจคอยู่ใน block ที่ TOH จัดการของ +`AGENTS.md` เพื่อความเข้ากันได้แบบเดิม ถ้าพิมพ์ `/toh-vibe ...` เป็นข้อความธรรมดา +ระบบจะตีความ (ผ่าน `AGENTS.md`) ว่าเป็นการเรียก skill ที่ตรงกัน — แต่มัน +**ไม่ใช่** native slash command ของ Codex TOH จะไม่อ่านหรือแก้ไขไฟล์ ZCode หรือ +global Codex configuration ถอนการติดตั้ง (ลบเฉพาะไฟล์ Codex ที่ TOH สร้าง — skill ของคุณเองและข้อความ -ใน `AGENTS.md` จะถูกเก็บไว้): +agent ของคุณ ไฟล์ ZCode และข้อความใน `AGENTS.md` จะถูกเก็บไว้): ```bash npx toh-framework uninstall --ide codex diff --git a/installer/ide-handlers/codex.js b/installer/ide-handlers/codex.js index 40270ff..7490efc 100644 --- a/installer/ide-handlers/codex.js +++ b/installer/ide-handlers/codex.js @@ -2,7 +2,8 @@ * Codex CLI IDE Handler * * Codex discovers repository skills from .agents/skills. TOH keeps the full - * runtime in .toh/ and installs thin wrappers for commands and skills. + * runtime in .toh/, installs thin wrappers for commands, and translates source + * agents to native project-scoped TOML files. */ import crypto from 'crypto'; @@ -10,6 +11,7 @@ import fs from 'fs-extra'; import path from 'path'; import { fileURLToPath } from 'url'; import yaml from 'js-yaml'; +import { parse as parseToml } from 'smol-toml'; import { renderCapabilitiesSection } from './shared.js'; const __filename = fileURLToPath(import.meta.url); @@ -18,6 +20,7 @@ const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '../../package.json' const VERSION = pkg.version; export const CODEX_SKILLS_DIR = path.join('.agents', 'skills'); +export const CODEX_AGENTS_DIR = path.join('.codex', 'agents'); export const AGENTS_MAX_BYTES = 24 * 1024; export const CODEX_PROJECT_DOC_MAX_BYTES = 64 * 1024; @@ -30,6 +33,17 @@ const CONFIG_BLOCK_RE = /[ \t]*# TOH-FRAMEWORK-START\r?\n[\s\S]*?[ \t]*# TOH-FRA const SKILL_GENERATOR = 'toh-framework'; const MANIFEST_PATH = path.join('.codex', 'toh-framework.json'); const SKILL_NAME_RE = /^[a-z0-9-]{1,64}$/; +const AGENT_MODEL_INTENTS = new Set(['lightweight', 'implementation', 'planning', 'review']); +const READ_ONLY_SOURCE_TOOLS = new Set(['Read', 'Grep', 'Glob', 'Bash']); + +// Codex model names change independently from Toh's abstract role names. Keep +// that translation in one table so a model refresh does not touch agent source. +export const CODEX_MODEL_ROUTING = Object.freeze({ + lightweight: Object.freeze({ model: 'gpt-5.6-luna', model_reasoning_effort: 'low' }), + implementation: Object.freeze({ model: 'gpt-5.6', model_reasoning_effort: 'medium' }), + planning: Object.freeze({ model: 'gpt-5.6', model_reasoning_effort: 'high' }), + review: Object.freeze({ model: 'gpt-5.6-terra', model_reasoning_effort: 'high' }) +}); function sha256(value) { return crypto.createHash('sha256').update(value).digest('hex'); @@ -76,15 +90,127 @@ async function fileMatchesHash(filePath, expectedHash) { } function parseFrontmatter(raw, label) { - const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/); + return parseFrontmatterDocument(raw, label).frontmatter; +} + +function parseFrontmatterDocument(raw, label) { + const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/); if (!match) throw new Error(`${label} must start with YAML frontmatter.`); try { - return yaml.load(match[1]) || {}; + return { frontmatter: yaml.load(match[1]) || {}, body: match[2] }; } catch (error) { throw new Error(`Invalid YAML frontmatter in ${label}: ${error.message}`); } } +function normalizeModelIntent(value) { + const intent = String(value || '').trim().toLowerCase().replaceAll('_', '-'); + const aliases = { + exploration: 'lightweight', + explore: 'lightweight', + scaffold: 'lightweight', + deep: 'planning', + 'deep-reasoning': 'planning', + security: 'review' + }; + return aliases[intent] || intent; +} + +export function resolveCodexModelIntent(agent) { + const explicit = normalizeModelIntent(agent.modelIntent || agent.model_intent || agent.codex?.modelIntent); + if (AGENT_MODEL_INTENTS.has(explicit)) return explicit; + + // Backward-compatible fallback for older source definitions. New definitions + // should use modelIntent so the source expresses intent, not a vendor tier. + const legacyTier = String(agent.model || '').trim().toLowerCase(); + if (legacyTier === 'haiku') return 'lightweight'; + if (legacyTier === 'opus') return 'planning'; + return 'implementation'; +} + +export async function readAgentCatalog(targetDir) { + const agentsDir = path.join(targetDir, '.toh', 'agents'); + if (!(await fs.pathExists(agentsDir))) return []; + + const files = (await fs.readdir(agentsDir, { withFileTypes: true })) + .filter((entry) => entry.isFile() && entry.name.endsWith('.md') && entry.name !== 'README.md') + .map((entry) => entry.name) + .sort(); + const catalog = []; + + for (const file of files) { + const sourcePath = path.join(agentsDir, file); + const { frontmatter, body } = parseFrontmatterDocument(await fs.readFile(sourcePath, 'utf8'), sourcePath); + const name = String(frontmatter.name || file.replace(/\.md$/, '')).trim(); + if (!SKILL_NAME_RE.test(name)) continue; + + const tools = Array.isArray(frontmatter.tools) ? frontmatter.tools.map(String) : []; + const skills = Array.isArray(frontmatter.skills) ? frontmatter.skills.map(String) : []; + const triggers = Array.isArray(frontmatter.triggers) ? frontmatter.triggers.map(String) : []; + catalog.push({ + name, + description: String(frontmatter.description || `${name} Toh Framework agent`).trim().slice(0, 1024), + body, + tools, + skills, + triggers, + modelIntent: resolveCodexModelIntent(frontmatter), + maxTurns: frontmatter.maxTurns + }); + } + + return catalog; +} + +function isReadOnlyAgent(agent) { + return agent.tools.length > 0 && agent.tools.every((tool) => READ_ONLY_SOURCE_TOOLS.has(tool)); +} + +export function translateAgentToCodex(agent) { + const model = CODEX_MODEL_ROUTING[agent.modelIntent] || CODEX_MODEL_ROUTING.implementation; + const skillRefs = agent.skills.length + ? `\nAssociated Toh skills (read these files before acting):\n${agent.skills + .map((skill) => `- .toh/skills/${skill}/SKILL.md`) + .join('\n')}` + : ''; + const toolBoundary = agent.tools.length + ? `\nSource tool boundary: ${agent.tools.join(', ')}. Codex maps this boundary to its sandbox; do not widen it.` + : ''; + const triggerHints = agent.triggers.length + ? `\nRouting hints from the canonical definition: ${agent.triggers.join('; ')}` + : ''; + const turnHint = agent.maxTurns !== undefined + ? `\nSource turn budget hint: ${agent.maxTurns}. Stay focused and return to the parent when the assigned work is complete.` + : ''; + const orchestrationContract = ` + +## Codex runtime contract +- Own only the task and files assigned by the parent orchestrator. +- Return a concise result with Status, Result, Evidence, Files, and Blockers. +- Run the task checkpoint when one is provided. A parent agent re-runs checkpoints before updating .toh/plan.md. +- Never parallelize dependent edits; parallel work is only for genuinely independent files. +${skillRefs}${toolBoundary}${triggerHints}${turnHint}`; + const instructions = `${agent.body.trim()}${orchestrationContract}`.trim(); + const sandboxMode = isReadOnlyAgent(agent) ? 'read-only' : 'workspace-write'; + const content = [ + `# Toh model intent: ${agent.modelIntent}`, + `name = ${JSON.stringify(agent.name)}`, + `description = ${JSON.stringify(agent.description)}`, + `model = ${JSON.stringify(model.model)}`, + `model_reasoning_effort = ${JSON.stringify(model.model_reasoning_effort)}`, + `sandbox_mode = ${JSON.stringify(sandboxMode)}`, + `developer_instructions = ${JSON.stringify(instructions)}`, + '' + ].join('\n'); + + try { + parseToml(content); + } catch (error) { + throw new Error(`Invalid generated Codex agent TOML for ${agent.name}: ${error.message}`); + } + return content; +} + export async function readCommandCatalog(srcDir) { const commandsDir = path.join(srcDir, 'commands'); if (!(await fs.pathExists(commandsDir))) { @@ -167,24 +293,20 @@ export async function readSupportingSkillCatalog(srcDir) { } async function readWrapperCatalog(srcDir) { - const [commands, skills] = await Promise.all([ - readCommandCatalog(srcDir), - readSupportingSkillCatalog(srcDir) - ]); - const wrappers = [ - ...skills, - ...commands.map((entry) => ({ + const commands = await readCommandCatalog(srcDir); + const wrappers = commands + .map((entry) => ({ ...entry, sourcePath: `.toh/commands/${entry.file}` })) - ].sort((a, b) => a.skillName.localeCompare(b.skillName)); + .sort((a, b) => a.skillName.localeCompare(b.skillName)); const names = new Set(); for (const wrapper of wrappers) { if (names.has(wrapper.skillName)) throw new Error(`Duplicate Codex skill name: ${wrapper.skillName}`); names.add(wrapper.skillName); } - return { commands, skills, wrappers }; + return { commands, wrappers }; } function wrapperFrontmatter(entry) { @@ -208,7 +330,7 @@ function renderCommandWrapper(entry) { .join('\n')}\n3. Execute the workflow in this session, in order.` : '2. Execute the workflow in this session, in order.'; - return `---\n${wrapperFrontmatter(entry)}\n---\n\n# ${entry.command} — ${entry.description}\n\n> Thin Codex wrapper for the TOH Framework workflow ${entry.command}.\n> Read the runtime workflow from \`${entry.sourcePath}\`; do not duplicate it here.\n\n## When to use\n\n${entry.description}. Triggers: ${triggers}, or any plain-language request that matches.\n\n## Workflow\n\n1. Read the full workflow definition: \`${entry.sourcePath}\`\n${supporting}\n\n## Codex constraints\n\n- **No subagents or teams** — execute delegated work inline, one task at a time.\n- **No Stop hook** — do not end while \`.toh/plan.md\` has unchecked, unblocked tasks.\n- **No model routing** — ignore model tiers in TOH docs.\n- **State** — persist work under \`.toh/\` and resume from the first unchecked task.\n`; + return `---\n${wrapperFrontmatter(entry)}\n---\n\n# ${entry.command} — ${entry.description}\n\n> Native Codex skill wrapper for the TOH Framework workflow ${entry.command}.\n> Read the runtime workflow from \`${entry.sourcePath}\`; do not duplicate it here.\n\n## When to use\n\n${entry.description}. Triggers: ${triggers}, or any plain-language request that matches.\n\n## Workflow\n\n1. Read the full workflow definition: \`${entry.sourcePath}\`\n${supporting}\n\n## Codex execution\n\n- Delegate independent plan tasks to the matching \`.codex/agents/.toml\`; keep dependent edits sequential.\n- The parent agent owns checkpoint verification and updates \`.toh/plan.md\` only after quoting passing output.\n- Codex has no Toh Stop hook; resume from the first unchecked task when a session ends.\n- Agent TOML files own model and reasoning routing; do not reinterpret Claude model names.\n`; } function renderSupportingSkillWrapper(entry) { @@ -216,9 +338,7 @@ function renderSupportingSkillWrapper(entry) { } function renderWrapper(entry) { - return entry.kind === 'command' - ? renderCommandWrapper(entry) - : renderSupportingSkillWrapper(entry); + return renderCommandWrapper(entry); } export async function installCodexSkills(targetDir, srcDir) { @@ -268,6 +388,64 @@ export async function installCodexSkills(targetDir, srcDir) { return installed; } +function agentFileRelativePath(name) { + return relativePath(CODEX_AGENTS_DIR, `${name}.toml`); +} + +export async function installCodexAgents(targetDir) { + const agentsDir = path.join(targetDir, '.toh', 'agents'); + if (!(await fs.pathExists(agentsDir))) return []; + + const agents = await readAgentCatalog(targetDir); + const agentsRoot = path.join(targetDir, CODEX_AGENTS_DIR); + await fs.ensureDir(agentsRoot); + const previous = await readManifest(targetDir); + const previousAgents = previous.agents || {}; + const nextAgents = {}; + const wanted = new Set(agents.map((agent) => agentFileRelativePath(agent.name))); + + for (const [relative, record] of Object.entries(previousAgents)) { + if (!relative.startsWith(`${CODEX_AGENTS_DIR}/`) || wanted.has(relative)) continue; + const filePath = path.join(targetDir, relative); + if (await fileMatchesHash(filePath, record.sha256)) { + await fs.remove(filePath); + const parent = path.dirname(filePath); + if ((await fs.readdir(parent)).length === 0) await fs.remove(parent); + } + } + + const installed = []; + for (const agent of agents) { + const relative = agentFileRelativePath(agent.name); + const filePath = path.join(targetDir, relative); + const content = translateAgentToCodex(agent); + const existingRecord = previousAgents[relative]; + const canReplace = !(await fs.pathExists(filePath)) || + await fileMatchesHash(filePath, existingRecord?.sha256); + + if (!canReplace) { + if (existingRecord) nextAgents[relative] = existingRecord; + continue; + } + await fs.ensureDir(path.dirname(filePath)); + await fs.writeFile(filePath, content); + nextAgents[relative] = { + sha256: sha256(content), + source: `.toh/agents/${agent.name}.md`, + modelIntent: agent.modelIntent + }; + installed.push(agent.name); + } + + await writeManifest(targetDir, { + ...previous, + generator: SKILL_GENERATOR, + version: VERSION, + agents: nextAgents + }); + return installed; +} + function renderSkillTable(wrappers) { const rows = wrappers .map((entry) => `| \`$${entry.skillName}\` | ${entry.description} |`) @@ -279,23 +457,26 @@ function generateAgentsBlock(wrappers, language) { const capabilities = renderCapabilitiesSection('codex'); const thai = language === 'th'; const title = thai - ? 'คุณคือ **Toh Framework Agent** ที่รันอยู่บน **Codex CLI** — ช่วย Solo Developer สร้าง SaaS คนเดียวจนจบ' - : 'You are the **Toh Framework Agent** running in **Codex CLI**, helping solo developers build SaaS systems by themselves.'; + ? 'คุณคือ **Toh Framework Agent** ที่รันอยู่บน Codex — ช่วย Solo Developer สร้าง SaaS คนเดียวจนจบ' + : 'You are the **Toh Framework Agent** running in Codex, helping solo developers build SaaS systems by themselves.'; const intro = thai - ? 'เวิร์กโฟลว์และกฎของ TOH ถูกติดตั้งเป็น native skills ไว้ที่ `.agents/skills/`:' - : 'TOH workflows and supporting rules are installed as native skills under `.agents/skills/`:'; + ? 'TOH มี workflow skills แบบ native และ custom agents แบบ native:' + : 'TOH provides native workflow skills and native custom agents:'; const runtime = thai ? '- `.toh/plan.md` + `.toh/progress.md` — แผนคือไฟล์และใช้ resume จาก task แรกที่ยังไม่ติ๊ก\n- `.toh/memory/` — memory 7 ไฟล์' : '- `.toh/plan.md` + `.toh/progress.md` — the plan is a file; resume from the first unchecked task\n- `.toh/memory/` — 7-file memory'; - const constraints = thai - ? '- **ไม่มี subagents/teams** — รัน THE TOH LOOP แบบ sequential ใน session นี้\n- **ไม่มี Stop hook** — ห้ามจบ session ถ้าแผนยังมี task ที่ไม่ได้ติ๊กและไม่ blocked\n- **ไม่มี model routing** — ข้าม tier haiku/sonnet/opus ในเอกสาร TOH' - : '- **No subagents or teams** — run THE TOH LOOP sequentially in this session\n- **No Stop hook** — never end while `.toh/plan.md` has unchecked, unblocked tasks\n- **No model routing** — ignore haiku/sonnet/opus tiers in TOH docs'; const compatibility = thai - ? '- เรียกตรงๆ ด้วย `$` หรือพิมพ์ `/skills` เพื่อดูทั้งหมด\n- แต่ละ wrapper อ่านเนื้อหาจริงจาก `.toh/commands/` หรือ `.toh/skills/`\n- ถ้าพิมพ์ `/toh-*` ให้ตีความเป็นคำขอใช้ skill ที่ตรงกัน ไม่ใช่ custom slash command' - : '- Invoke explicitly with `$` or browse with `/skills`, or describe the task for implicit matching.\n- Each wrapper reads the full source from `.toh/commands/` or `.toh/skills/`.\n- If the user types `/toh-*`, interpret it as a request for the matching skill, not a custom slash command.'; + ? '- `$` หรือ `/skills` เพื่อดู workflow\n- แต่ละ workflow อ่านคำสั่งจริงจาก `.toh/commands/` และ skill ภายในจาก `.toh/skills/`\n- ถ้าพิมพ์ `/toh-*` ให้ตีความเป็น backward-compatible plain-text request ไม่ใช่ native slash command' + : '- Invoke a workflow with `$` or browse with `/skills`.\n- Each workflow reads its command and internal skills from `.toh/`.\n- If the user types `/toh-*`, interpret it as a backward-compatible plain-text request, not a native slash command.'; + const agents = thai + ? '- `.codex/agents/*.toml` — custom agents ที่สร้างจาก `.toh/agents/*.md`\n- ใช้ agent ตามชื่อใน task ของ plan; native agent เป็นผู้กำหนด model, reasoning และ sandbox' + : '- `.codex/agents/*.toml` — custom agents generated from `.toh/agents/*.md`\n- Use the agent named by each plan task; native agent files define model, reasoning, and sandbox.'; + const execution = thai + ? '- เริ่มจาก task แรกที่ยังไม่ติ๊กใน `.toh/plan.md`\n- delegate เฉพาะงานที่อิสระจริงและไฟล์ไม่ทับกัน; งานที่พึ่งกันทำตามลำดับ\n- parent ต้องรัน checkpoint เองก่อนติ๊ก task\n- ผลจาก agent ต้องมี Status, Result, Evidence, Files, Blockers; ถ้าล้มเหลวส่งกลับ parent เพื่อแก้หรือ mark blocked ตาม protocol' + : '- Start with the first unchecked task in `.toh/plan.md`.\n- Delegate only genuinely independent work on disjoint files; keep dependent work sequential.\n- The parent re-runs each checkpoint before ticking a task.\n- Agent results must include Status, Result, Evidence, Files, and Blockers; failures return to the parent for repair or blocking per the protocol.'; return `${AGENTS_BLOCK_START} -# 🎯 Toh Framework +# Toh Framework > **"Type Once, Have it all!"** — AI-Orchestration Driven Development${thai ? '\n> "สั่งครั้งเดียว จบครบโดยไม่ต้องถาม"' : ''} @@ -318,9 +499,17 @@ ${compatibility} ${runtime} - \`.toh/skills/\` · \`.toh/commands/\` · \`.toh/capabilities.json\` -## ${thai ? 'ข้อจำกัดของ Codex' : 'Codex constraints'} +${agents} + +## TOH execution contract + +${execution} -${constraints} +## ${thai ? 'ขอบเขตของ Codex' : 'Codex boundary'} + +- Codex native agents and multi-agent tools are Codex CLI capabilities. +- Installation does not disable multi-agent support when the \`codex\` CLI is absent; an unknown probe result remains eligible for native behavior. +- Codex has no Toh Stop hook. Completion requires the workflow's own quoted checkpoint evidence. ${AGENTS_BLOCK_END}`; } @@ -349,22 +538,50 @@ export async function updateAgentsMd(targetDir, wrappers, language = 'en') { return agentsPath; } -function codexConfigBlock() { - return `${CONFIG_BLOCK_START}\n# Keep Codex project instructions below its discovery ceiling.\nproject_doc_max_bytes = ${CODEX_PROJECT_DOC_MAX_BYTES}\n${CONFIG_BLOCK_END}`; +function codexConfigBlock(enableMultiAgent = true, enableProjectDocQuota = true) { + const lines = [ + CONFIG_BLOCK_START, + '# Enable Codex native delegation and keep project instructions below its discovery ceiling.' + ]; + if (enableProjectDocQuota) lines.push(`project_doc_max_bytes = ${CODEX_PROJECT_DOC_MAX_BYTES}`); + if (enableMultiAgent) lines.push('[features]', 'multi_agent = true'); + lines.push(CONFIG_BLOCK_END); + return `${lines.join('\n')}\n`; +} + +function insertCodexConfigBlock(content, block, needsRootKey) { + if (!content) return `${block}\n`; + if (!needsRootKey) return `${content}\n\n${block}\n`; + + const firstTable = content.search(/^\s*\[/m); + if (firstTable === -1) return `${content}\n\n${block}\n`; + return `${content.slice(0, firstTable).trimEnd()}\n\n${block}\n${content.slice(firstTable)}`; } export async function setupCodexConfig(targetDir) { const configPath = path.join(targetDir, '.codex', 'config.toml'); const existing = await fs.pathExists(configPath) ? await fs.readFile(configPath, 'utf8') : ''; const withoutToh = existing.replace(CONFIG_BLOCK_RE, '').trimEnd(); - const hasUserQuota = /^\s*project_doc_max_bytes\s*=/m.test(withoutToh); - if (hasUserQuota) { + let hasUserQuota = false; + let hasUserFeatures = false; + if (withoutToh) { + try { + const parsed = parseToml(withoutToh); + hasUserQuota = Object.hasOwn(parsed, 'project_doc_max_bytes'); + hasUserFeatures = Boolean(parsed.features && typeof parsed.features === 'object'); + } catch { + const rootSection = withoutToh.split(/^\s*\[/m, 1)[0]; + hasUserQuota = /^\s*project_doc_max_bytes\s*=\s*/m.test(rootSection); + hasUserFeatures = /^\s*\[features\]\s*$/m.test(withoutToh); + } + } + if (hasUserQuota && hasUserFeatures) { await updateManifest(targetDir, { configSha256: null }); return configPath; } - const block = codexConfigBlock(); - const next = withoutToh ? `${withoutToh}\n\n${block}\n` : `${block}\n`; + const block = codexConfigBlock(!hasUserFeatures, !hasUserQuota); + const next = insertCodexConfigBlock(withoutToh, block, !hasUserQuota); await fs.ensureDir(path.dirname(configPath)); await fs.writeFile(configPath, next); await updateManifest(targetDir, { configSha256: sha256(next) }); @@ -408,8 +625,9 @@ export async function setupCodex(targetDir, srcDir, language = 'en') { const { wrappers } = await readWrapperCatalog(srcDir); await setupCodexConfig(targetDir); const installed = await installCodexSkills(targetDir, srcDir); + const installedAgents = await installCodexAgents(targetDir); await updateAgentsMd(targetDir, wrappers, language); - return `${CODEX_SKILLS_DIR}/ (${installed.length}/${wrappers.length} wrappers) + AGENTS.md + .codex/config.toml`; + return `${CODEX_SKILLS_DIR}/ (${installed.length}/${wrappers.length} workflows) + ${CODEX_AGENTS_DIR}/ (${installedAgents.length} agents) + AGENTS.md + .codex/config.toml`; } async function makeBackup(targetDir, files) { @@ -429,6 +647,7 @@ export async function uninstallCodex(targetDir, options = {}) { const { dryRun = false, backup = true } = options; const manifest = await readManifest(targetDir); const skillRemovals = []; + const agentRemovals = []; const backupFiles = []; for (const [relative, record] of Object.entries(manifest.files || {})) { @@ -439,6 +658,14 @@ export async function uninstallCodex(targetDir, options = {}) { backupFiles.push(relative); } + for (const [relative, record] of Object.entries(manifest.agents || {})) { + if (!relative.startsWith(`${CODEX_AGENTS_DIR}/`) || !relative.endsWith('.toml')) continue; + const filePath = path.join(targetDir, relative); + if (!(await fileMatchesHash(filePath, record.sha256))) continue; + agentRemovals.push({ relative, filePath, name: path.basename(filePath, '.toml') }); + backupFiles.push(relative); + } + let agentsAction = 'absent'; const agentsPath = path.join(targetDir, 'AGENTS.md'); if (await fs.pathExists(agentsPath) && manifest.agentsBlockSha256) { @@ -461,6 +688,7 @@ export async function uninstallCodex(targetDir, options = {}) { const result = { removedSkills: skillRemovals.map((item) => item.name).sort(), + removedAgents: agentRemovals.map((item) => item.name).sort(), agentsMd: agentsAction, config: configAction, dryRun, @@ -476,6 +704,12 @@ export async function uninstallCodex(targetDir, options = {}) { if ((await fs.readdir(parent)).length === 0) await fs.remove(parent); } + for (const item of agentRemovals) { + await fs.remove(item.filePath); + const parent = path.dirname(item.filePath); + if ((await fs.readdir(parent)).length === 0) await fs.remove(parent); + } + if (agentsAction !== 'absent') { const existing = await fs.readFile(agentsPath, 'utf8'); const stripped = existing.replace(AGENTS_BLOCK_RE, '').trim(); diff --git a/installer/ide-handlers/shared.js b/installer/ide-handlers/shared.js index 89c07cd..189bd81 100644 --- a/installer/ide-handlers/shared.js +++ b/installer/ide-handlers/shared.js @@ -23,7 +23,7 @@ const VERSION = pkg.version; // ============================================================ // 1. Capability profiles (per IDE) // ============================================================ -// subagents: 'native' | 'none' +// subagents: 'native' | 'none' | 'unknown' // teams: 'env-gated' | false (env CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS) // goal: 'version-gated' | false (Claude Code >= 2.1.139) // workflows: 'version-gated' | false (Claude Code >= 2.1.154, can be plan-disabled) @@ -52,14 +52,17 @@ export const CAPABILITY_PROFILES = { }, codex: { ide: 'codex', - subagents: 'none', + client: 'codex-cli', + detection: 'declared-at-install', + subagents: 'native', teams: false, goal: false, loop: false, hooks: false, - workflows: false, - parallel: false, - modelRouting: false + workflows: 'native', + parallel: true, + modelRouting: true, + nativeAgents: true }, 'gemini-cli': { ide: 'gemini-cli', @@ -228,9 +231,10 @@ export function renderCapabilitiesSection(ide) { const profile = CAPABILITY_PROFILES[key]; const display = IDE_DISPLAY[key] || { name: key || 'unknown runtime', contextFile: 'this context file' }; - // Unknown IDE -> conservative sequential profile + // Unknown IDE -> conservative profile. Unknown is not a reason to disable a + // capability supplied by a client that the installer cannot probe. const p = profile || { - ide: key, subagents: 'none', teams: false, goal: false, + ide: key, subagents: 'unknown', teams: false, goal: false, loop: false, hooks: false, workflows: false, parallel: false, modelRouting: false }; @@ -239,8 +243,12 @@ export function renderCapabilitiesSection(ide) { 'Never guess your runtime; it is stated here.'; const lines = []; - if (p.subagents === 'native') { + if (p.subagents === 'native' && key === 'codex') { + lines.push('- Native subagents: YES — delegate via `.codex/agents/*.toml` (parallel only for independent tasks on disjoint files, max 4 concurrent)'); + } else if (p.subagents === 'native') { lines.push('- Native subagents: YES — delegate via the Task tool (parallel only for independent tasks on disjoint files, max 4 concurrent)'); + } else if (p.subagents === 'unknown') { + lines.push('- Native subagents: UNKNOWN — keep native delegation eligible; do not disable it from an unavailable probe'); } else { lines.push('- Native subagents: NO — single-session only'); } @@ -258,10 +266,17 @@ export function renderCapabilitiesSection(ide) { : '- Hooks: NO'); lines.push(p.workflows === 'version-gated' ? '- Workflows: version-gated (Claude Code >= 2.1.154)' + : p.workflows === 'native' + ? '- Workflows: YES — invoke installed native skills with `$toh-*`' : '- Workflows: NO'); lines.push(p.modelRouting - ? '- Model routing: YES — haiku = scaffold/tests · sonnet = builders · opus = planning/QC' + ? key === 'codex' + ? '- Model routing: YES — native agent TOML sets Codex model and reasoning per Toh role' + : '- Model routing: YES — haiku = scaffold/tests · sonnet = builders · opus = planning/QC' : '- Model routing: NO — ignore model tiers and proceed'); + if (p.nativeAgents) { + lines.push('- Native agent files: `.codex/agents/*.toml` — generated from `.toh/agents/*.md` with ownership-safe updates'); + } if (!p.parallel) { lines.push('- Execution mode: run THE TOH LOOP **sequentially in this session** (orchestration-protocol skill); recovery = checkbox-resume from `.toh/plan.md`'); } diff --git a/installer/install.js b/installer/install.js index af3a011..e2fd293 100644 --- a/installer/install.js +++ b/installer/install.js @@ -12,7 +12,7 @@ import { dirname, join } from 'path'; import { setupClaudeCode } from './ide-handlers/claude-code.js'; import { setupCursor } from './ide-handlers/cursor.js'; import { setupGeminiCLI } from './ide-handlers/gemini-cli.js'; -import { setupCodex, uninstallCodex } from './ide-handlers/codex.js'; +import { CODEX_AGENTS_DIR, CODEX_SKILLS_DIR, setupCodex, uninstallCodex } from './ide-handlers/codex.js'; import { transformCommand, writeCapabilitiesJson } from './ide-handlers/shared.js'; const __filename = fileURLToPath(import.meta.url); @@ -273,7 +273,7 @@ function getIDEConfigFile(ideName) { 'Claude Code': 'created CLAUDE.md', 'Cursor': '.cursor/rules/*.mdc', 'Gemini CLI': '.gemini/GEMINI.md', - 'Codex CLI': '.agents/skills/ + AGENTS.md + .codex/config.toml' + 'Codex CLI': `${CODEX_SKILLS_DIR}/ + ${CODEX_AGENTS_DIR}/ + AGENTS.md + .codex/config.toml` }; return configs[ideName] || 'configured'; } @@ -674,7 +674,8 @@ function printNextSteps(config) { // 13 green + 47 gray = 60 ; 11 green + 49 gray = 60 ; 18 green + 42 gray = 60 console.log(row(chalk.green(' $toh-vibe') + chalk.gray(' - Native skill: new project'.padEnd(47)))); console.log(row(chalk.green(' /skills') + chalk.gray(' - Browse all TOH skills'.padEnd(49)))); - console.log(row(chalk.green(' .agents/skills/') + chalk.gray(' - 37 native wrappers installed'.padEnd(42)))); + console.log(row(chalk.green(` ${CODEX_SKILLS_DIR}/`) + chalk.gray(' - 14 workflow skills installed'.padEnd(42)))); + console.log(row(chalk.green(` ${CODEX_AGENTS_DIR}/`) + chalk.gray(' - 8 native agents installed'.padEnd(42)))); console.log(empty); } diff --git a/installer/uninstall.js b/installer/uninstall.js index 6ca164a..b758cba 100644 --- a/installer/uninstall.js +++ b/installer/uninstall.js @@ -4,15 +4,15 @@ * Removes TOH-managed files from a target project. * * Scoping rules: - * --ide codex -> only Codex-owned TOH files (.agents/skills managed - * skills + the AGENTS.md TOH block). `.toh/` is shared + * --ide codex -> only Codex-owned TOH files (.agents/skills and + * .codex/agents managed files + the AGENTS.md TOH block). `.toh/` is shared * runtime state and stays — other IDEs may still use it. * (no --ide) -> full removal: every IDE surface the installer owns, * plus `.toh/`. * * User content is never deleted: user skills under .agents/skills, user text - * in AGENTS.md outside the TOH markers, and files we cannot classify are - * all left untouched. + * in AGENTS.md outside the TOH markers, ZCode files, and files we cannot + * classify are all left untouched. */ import chalk from 'chalk'; @@ -48,9 +48,10 @@ export async function uninstall(options) { // ---- Codex teardown (per-IDE and full uninstall both do this) ---- const spinner = ora('Removing Codex integration...').start(); try { - const { removedSkills, agentsMd, config, backupPath } = await uninstallCodex(targetDir, { dryRun, backup }); + const { removedSkills, removedAgents, agentsMd, config, backupPath } = await uninstallCodex(targetDir, { dryRun, backup }); const parts = []; parts.push(removedSkills.length ? `${removedSkills.length} skill(s) ${dryRun ? 'would be removed' : 'removed'}` : 'no TOH skills found'); + if (removedAgents.length) parts.push(`${removedAgents.length} agent(s) ${dryRun ? 'would be removed' : 'removed'}`); if (agentsMd === 'updated') parts.push(`AGENTS.md block ${dryRun ? 'would be removed' : 'removed'}`); if (agentsMd === 'removed') parts.push(`AGENTS.md ${dryRun ? 'would be removed' : 'removed (was TOH-only)'}`); if (config === 'updated' || config === 'removed') parts.push(`Codex config ${dryRun ? 'would be updated' : 'updated'}`); diff --git a/package-lock.json b/package-lock.json index 3ff562f..60e49ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,8 @@ "fs-extra": "^11.2.0", "inquirer": "^9.2.23", "js-yaml": "^4.3.0", - "ora": "^8.0.1" + "ora": "^8.0.1", + "smol-toml": "^1.8.0" }, "bin": { "toh": "bin/toh-cli.js", @@ -789,6 +790,18 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/smol-toml": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.8.0.tgz", + "integrity": "sha512-kCZr2V3ch9i00x8zXRhjUNVcjG9ijES5dDudkXvUVCT5QlJNQWElSJdZqyPemffHoLNUYwOcou0Fy+ojN0uHSQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 18" + }, + "funding": { + "url": "https://github.com/sponsors/cyyynthia" + } + }, "node_modules/stdin-discarder": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", diff --git a/package.json b/package.json index 6fc3eec..d73bf04 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "toh-framework", "version": "2.1.0", "type": "module", - "description": "AI-Orchestration Driven Development - Type Once, Have it all! Approve once and the TOH LOOP builds, tests, and fixes a whole app until verified DONE. For Claude Code, Cursor, Codex, Gemini CLI, and Antigravity.", + "description": "AI-Orchestration Driven Development - Type Once, Have it all! Approve once and the TOH LOOP builds, tests, and fixes a whole app until verified DONE. For Claude Code, Cursor, Codex CLI, Gemini CLI, and Antigravity.", "author": { "name": "Wasin Treesinthuros", "email": "dr.wasin@gmail.com" @@ -62,6 +62,7 @@ "fs-extra": "^11.2.0", "inquirer": "^9.2.23", "js-yaml": "^4.3.0", - "ora": "^8.0.1" + "ora": "^8.0.1", + "smol-toml": "^1.8.0" } } diff --git a/src/agents/README.md b/src/agents/README.md index 825e990..87cc8ea 100644 --- a/src/agents/README.md +++ b/src/agents/README.md @@ -16,7 +16,7 @@ src/agents/*.md ← single source (superset frontmatter + canonical ├── Claude Code → copy as-is → .claude/agents/*.md │ (uses native name / description / tools / model) ├── Cursor → strip frontmatter → bundle into a .mdc rules file - ├── Codex → generate thin wrappers in .agents/skills/; keep runtime body in .toh/ + ├── Codex → generate native agents in .codex/agents/ + command skills in .agents/skills/ └── Gemini / Antigravity → convert frontmatter to each IDE's format ``` @@ -36,6 +36,7 @@ tools: # Claude Code: native tool allowlist - Edit - Bash model: sonnet # Claude Code: model tier per agent +modelIntent: implementation # Codex: lightweight | implementation | planning | review skills: # Toh skill bindings (all IDEs) - ui-first-builder - design-craft @@ -78,7 +79,7 @@ The same source produces IDE-appropriate output at install time: |-----|----------------|------------------------| | Claude Code | `.claude/agents/*.md` | Copied as-is (native `name`/`description`/`tools`/`model`) | | Cursor | `.cursor/rules/…` | Frontmatter stripped → bundled as rules | -| Codex CLI | `.agents/skills/*.md` + `AGENTS.md` | 23 supporting-skill + 14 command wrappers; runtime body stays in `.toh/` | +| Codex CLI | `.codex/agents/*.toml` + `.agents/skills/*/SKILL.md` + `AGENTS.md` | 8 native agents + 14 workflow skills; 23 supporting skills stay in `.toh/` | | Gemini / Antigravity | `.toh/agents/*.md` | Frontmatter converted per IDE | ``` @@ -94,6 +95,8 @@ The same source produces IDE-appropriate output at install time: ``` There is no `subagents/` folder anymore — the installer is the transform layer. +Codex model names and reasoning are selected centrally from each agent's +`modelIntent`; Claude's `model` tier remains for Claude Code compatibility. --- diff --git a/src/agents/backend-connector.md b/src/agents/backend-connector.md index 105ea3a..e12000d 100644 --- a/src/agents/backend-connector.md +++ b/src/agents/backend-connector.md @@ -11,6 +11,7 @@ tools: - Edit - Bash model: sonnet +modelIntent: implementation skills: - backend-engineer # Core backend / Supabase skills - engineer-harness # Smart tool selection + human-friendly reporting + next steps diff --git a/src/agents/design-reviewer.md b/src/agents/design-reviewer.md index e31c4b6..776d352 100644 --- a/src/agents/design-reviewer.md +++ b/src/agents/design-reviewer.md @@ -13,6 +13,7 @@ tools: - Edit - Bash model: opus +modelIntent: review memory: project skills: - design-craft # Two-pass identity process + AVOID-LIST + usability floor diff --git a/src/agents/dev-builder.md b/src/agents/dev-builder.md index 241759d..cd2bafc 100644 --- a/src/agents/dev-builder.md +++ b/src/agents/dev-builder.md @@ -12,6 +12,7 @@ tools: - Bash - WebFetch model: sonnet +modelIntent: implementation isolation: worktree skills: - dev-engineer # Core dev skills diff --git a/src/agents/plan-orchestrator.md b/src/agents/plan-orchestrator.md index 0b7cd88..1cdcdc6 100644 --- a/src/agents/plan-orchestrator.md +++ b/src/agents/plan-orchestrator.md @@ -14,6 +14,7 @@ tools: - Bash - WebFetch model: opus +modelIntent: planning memory: project skills: - plan-orchestrator # Planning + plan artifact + single-gate handoff diff --git a/src/agents/platform-adapter.md b/src/agents/platform-adapter.md index fcf78c0..5afac65 100644 --- a/src/agents/platform-adapter.md +++ b/src/agents/platform-adapter.md @@ -12,6 +12,7 @@ tools: - Edit - Bash model: sonnet +modelIntent: implementation skills: - platform-specialist # Core platform adaptation skills (doc-driven) - engineer-harness # Human-friendly reporting + next steps diff --git a/src/agents/root-cause-debugger.md b/src/agents/root-cause-debugger.md index d4442ef..19025fc 100644 --- a/src/agents/root-cause-debugger.md +++ b/src/agents/root-cause-debugger.md @@ -11,6 +11,7 @@ tools: - Glob - Bash model: sonnet +modelIntent: review skills: - debug-protocol - error-handling diff --git a/src/agents/test-runner.md b/src/agents/test-runner.md index eb5de1b..3829c92 100644 --- a/src/agents/test-runner.md +++ b/src/agents/test-runner.md @@ -11,6 +11,7 @@ tools: - Edit - Bash model: haiku +modelIntent: lightweight maxTurns: 30 skills: - test-engineer # Core testing skills diff --git a/src/agents/ui-builder.md b/src/agents/ui-builder.md index 9cb9c19..fcfb8dc 100644 --- a/src/agents/ui-builder.md +++ b/src/agents/ui-builder.md @@ -11,6 +11,7 @@ tools: - Edit - Bash model: sonnet +modelIntent: implementation isolation: worktree skills: - ui-first-builder # Core UI building methodology diff --git a/src/skills/orchestration-protocol/SKILL.md b/src/skills/orchestration-protocol/SKILL.md index b023724..478527b 100644 --- a/src/skills/orchestration-protocol/SKILL.md +++ b/src/skills/orchestration-protocol/SKILL.md @@ -38,7 +38,7 @@ Your runtime identity is declared by the platform context file that loaded you: | `AGENTS.md` | Codex | | `GEMINI.md` | Gemini CLI / Antigravity | -Confirm capabilities from `.toh/capabilities.json` (written by the installer). If it is missing, infer conservatively: only Claude Code has subagents, teams, hooks, `/goal`, `/loop`; every other runtime is single-session sequential. +Confirm capabilities from `.toh/capabilities.json` (written by the installer). If it is missing, infer conservatively: Claude Code has Task subagents, teams, hooks, `/goal`, and `/loop`; Codex has native custom agents and native skill workflows when its client supports them; Cursor, Gemini, and Antigravity are single-session sequential. An unknown capability probe never disables a client-native feature. ### Step 2 — Runtime probe (ONLY for what install time cannot know) @@ -59,7 +59,7 @@ Do NOT invent other detection heuristics. Identity comes from Step 1; the probe Three rungs, best first. **Each rung: if unavailable, fall back one rung.** Sequential is the floor and is always available. 1. **AGENT TEAMS** — Claude Code with the teams env flag set, AND the plan has >= 3 independent modules plus a QC role. Recipe in Section F. If unavailable, fall back one rung. -2. **NATIVE SUBAGENTS** — the Task/Agent tool exists. Delegate tasks to TFW agents; parallel only under the rules below. If unavailable, fall back one rung. +2. **NATIVE SUBAGENTS** — Claude's Task/Agent tool or Codex's `.codex/agents/*.toml` is available. Delegate tasks to TFW agents; parallel only under the rules below. If unavailable, fall back one rung. 3. **SEQUENTIAL SELF** — execute every task yourself, in order, in this session. This is the default mode and the correct choice more often than not. ### When to use which @@ -69,7 +69,8 @@ Three rungs, best first. **Each rung: if unavailable, fall back one rung.** Sequ | <= 3 tasks total | SEQUENTIAL | | Same-file or dependent edits | SEQUENTIAL | | Debugging / fixing | SEQUENTIAL | -| Runtime without subagents (Cursor / Codex / Gemini / Antigravity) | SEQUENTIAL | +| Runtime without subagents (Cursor / Gemini / Antigravity) | SEQUENTIAL | +| Codex with native agents | NATIVE SUBAGENTS | | >= 2 independent tasks on disjoint files, each substantial (~5+ min) | PARALLEL subagents | | MVP-scale: >= 3 independent modules + a QC role, teams flag set | TEAMS | @@ -107,7 +108,7 @@ Mirrors TFW agent frontmatter; teams and subagents both honor per-agent `model` | **sonnet** | Builders — ui-builder, dev-builder, implementation work | | **opus** | Planning, QC/review, design review | -On runtimes without model routing, ignore this table and proceed. +On runtimes without model routing, ignore this table and proceed. Codex native agents use the generated `model` and `model_reasoning_effort` fields; do not map Claude tier names at runtime. --- @@ -224,7 +225,7 @@ Section E is the floor on every runtime. On Claude Code the installer ships mach | **`/goal` recipe** (>= 2.1.139) | Set the finish line before coding: `/goal every task in .toh/plan.md is checked and the build command exits 0 — or stop after 40 turns`. A Haiku evaluator judges the condition FROM THE TRANSCRIPT — one more reason the QC gate quotes actual output: unquoted results are invisible to the evaluator. | | **Workflows** (>= 2.1.154, optional) | `/toh-sweep` (not shipped — optional pattern you can save to `.claude/workflows/`) can fan out fixers per failing task until checks pass. | -**Every other runtime** (Cursor / Codex / Gemini / Antigravity) runs the SAME loop as prose in one session — no hooks, no `/goal`. The recovery mechanism there is checkbox-resume: a fresh session picks up at the first unchecked task. If context runs low mid-plan, flush state (plan checkboxes + progress.md + active.md pointer), then tell the user to re-run the command — it resumes exactly where it stopped. +**Every other runtime** (Cursor / Gemini / Antigravity) runs the SAME loop as prose in one session — no hooks, no `/goal`. Codex also runs the same loop, but may delegate independent tasks to generated native agents; its parent still owns checkpoint verification and checkbox updates. The recovery mechanism there is checkbox-resume: a fresh session picks up at the first unchecked task. If context runs low mid-plan, flush state (plan checkboxes + progress.md + active.md pointer), then tell the user to re-run the command — it resumes exactly where it stopped. --- diff --git a/src/skills/smart-routing/SKILL.md b/src/skills/smart-routing/SKILL.md index b9c42b6..68b0fef 100644 --- a/src/skills/smart-routing/SKILL.md +++ b/src/skills/smart-routing/SKILL.md @@ -141,7 +141,8 @@ Probe exactly: the `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS` env flag, plus the Cla Choose from the **execution ladder in `orchestration-protocol` (Section B)** — the full decision table lives there, once. Summary only: - **Claude Code** → ladder: teams > subagents > sequential -- **Cursor / Codex** → sequential TOH LOOP in-session +- **Cursor** → sequential TOH LOOP in-session +- **Codex CLI** → native agents for independent tasks; sequential TOH LOOP for dependent work - **Gemini / Antigravity** → sequential prose loop --- diff --git a/tests/codex.test.js b/tests/codex.test.js index 9eaddd6..e382591 100644 --- a/tests/codex.test.js +++ b/tests/codex.test.js @@ -26,18 +26,25 @@ import os from 'os'; import path from 'path'; import { fileURLToPath } from 'url'; import yaml from 'js-yaml'; +import { parse as parseToml } from 'smol-toml'; import { install } from '../installer/install.js'; import { AGENTS_MAX_BYTES, + CODEX_AGENTS_DIR, + CODEX_MODEL_ROUTING, CODEX_SKILLS_DIR, assertAgentsMdSize, setupCodex, uninstallCodex, installCodexSkills, + readAgentCatalog, readCommandCatalog, - readSupportingSkillCatalog + readSupportingSkillCatalog, + setupCodexConfig, + translateAgentToCodex } from '../installer/ide-handlers/codex.js'; +import { CAPABILITY_PROFILES, renderCapabilitiesSection } from '../installer/ide-handlers/shared.js'; const __filename = fileURLToPath(import.meta.url); const REPO_ROOT = path.join(path.dirname(__filename), '..'); @@ -60,6 +67,17 @@ const EXPECTED_COMMANDS = [ 'toh-vibe' ]; +const EXPECTED_AGENTS = [ + 'backend-connector', + 'design-reviewer', + 'dev-builder', + 'plan-orchestrator', + 'platform-adapter', + 'root-cause-debugger', + 'test-runner', + 'ui-builder' +]; + // ---------------------------------------------------------------- helpers async function makeTmpProject() { @@ -104,13 +122,27 @@ test('fresh install creates .toh/, .agents/skills/, config.toml and AGENTS.md', assert.ok(await fs.pathExists(path.join(dir, '.toh', 'skills', 'orchestration-protocol', 'SKILL.md')), '.toh skills installed'); assert.ok(await fs.pathExists(path.join(dir, 'AGENTS.md')), 'AGENTS.md exists'); assert.ok(await fs.pathExists(path.join(dir, '.codex', 'config.toml')), 'Codex config exists'); - assert.match(await fs.readFile(path.join(dir, '.codex', 'config.toml'), 'utf8'), /project_doc_max_bytes\s*=\s*65536/); + const config = await fs.readFile(path.join(dir, '.codex', 'config.toml'), 'utf8'); + const parsedConfig = parseToml(config); + assert.equal(parsedConfig.project_doc_max_bytes, 65536, 'project doc quota is a root-level setting'); + assert.equal(parsedConfig.features.multi_agent, true, 'native delegation is enabled'); assert.ok(!(await fs.pathExists(path.join(dir, '.codex', 'skills'))), 'legacy .codex/skills is not used'); + const generatedAgents = (await fs.readdir(path.join(dir, CODEX_AGENTS_DIR))).sort(); + assert.deepEqual(generatedAgents, EXPECTED_AGENTS.map((name) => `${name}.toml`), 'all eight native agents generated'); + const manifest = await fs.readJson(path.join(dir, '.codex', 'toh-framework.json')); + assert.equal(Object.keys(manifest.agents).length, 8, 'native agents are ownership-tracked'); + const rootCause = parseToml(await fs.readFile(path.join(dir, CODEX_AGENTS_DIR, 'root-cause-debugger.toml'), 'utf8')); + assert.equal(rootCause.sandbox_mode, 'read-only', 'read-only source agent maps to Codex read-only sandbox'); + assert.equal(rootCause.model, CODEX_MODEL_ROUTING.review.model); + assert.equal(rootCause.model_reasoning_effort, CODEX_MODEL_ROUTING.review.model_reasoning_effort); + assert.ok(rootCause.developer_instructions.includes('Status, Result, Evidence, Files, and Blockers')); + assert.ok((await fs.readFile(path.join(dir, 'AGENTS.md'), 'utf8')).includes('.codex/agents/*.toml')); + const supporting = await readSupportingSkillCatalog(SRC_DIR); assert.equal(supporting.length, 23, 'all 23 supporting skills are catalogued'); - assert.equal((await fs.readdir(path.join(dir, CODEX_SKILLS_DIR))).length, 37, '14 commands + 23 skills are wrapped'); - for (const skill of [...supporting.map((entry) => entry.skillName), ...EXPECTED_COMMANDS]) { + assert.equal((await fs.readdir(path.join(dir, CODEX_SKILLS_DIR))).length, 14, '14 workflow commands are wrapped'); + for (const skill of EXPECTED_COMMANDS) { assert.ok( await fs.pathExists(path.join(dir, CODEX_SKILLS_DIR, skill, 'SKILL.md')), `native skill installed: ${skill}` @@ -121,6 +153,101 @@ test('fresh install creates .toh/, .agents/skills/, config.toml and AGENTS.md', } }); +test('native agent translation preserves source intents and valid TOML', async () => { + const dir = await makeTmpProject(); + try { + await quickInstallCodex(dir); + const catalog = await readAgentCatalog(dir); + assert.deepEqual(catalog.map((agent) => agent.name), EXPECTED_AGENTS); + for (const agent of catalog) { + const parsed = parseToml(translateAgentToCodex(agent)); + assert.equal(parsed.name, agent.name); + assert.equal(parsed.model, CODEX_MODEL_ROUTING[agent.modelIntent].model); + assert.equal(parsed.model_reasoning_effort, CODEX_MODEL_ROUTING[agent.modelIntent].model_reasoning_effort); + assert.equal(typeof parsed.developer_instructions, 'string'); + } + } finally { + await fs.remove(dir); + } +}); + +test('Codex CLI capabilities are native without a CLI binary probe', () => { + const profile = CAPABILITY_PROFILES.codex; + assert.equal(profile.client, 'codex-cli'); + assert.equal(profile.detection, 'declared-at-install'); + assert.equal(profile.subagents, 'native'); + assert.equal(profile.parallel, true); + assert.equal(profile.modelRouting, true); + const section = renderCapabilitiesSection('codex'); + assert.match(section, /\.codex\/agents/); + assert.match(section, /Codex model and reasoning/); + assert.doesNotMatch(section, /single-session only/); +}); + +test('existing Codex feature and quota settings are not duplicated', async () => { + const dir = await makeTmpProject(); + try { + const configPath = path.join(dir, '.codex', 'config.toml'); + const userConfig = 'project_doc_max_bytes = 12345\n[features]\nmulti_agent = false\n'; + await fs.ensureDir(path.dirname(configPath)); + await fs.writeFile(configPath, userConfig); + + await setupCodexConfig(dir); + assert.equal(await fs.readFile(configPath, 'utf8'), userConfig, 'user-owned Codex settings stay unchanged'); + } finally { + await fs.remove(dir); + } +}); + +test('existing Codex features keep their value while root quota is added', async () => { + const dir = await makeTmpProject(); + try { + const configPath = path.join(dir, '.codex', 'config.toml'); + const userConfig = '[features]\nmulti_agent = false\n'; + await fs.ensureDir(path.dirname(configPath)); + await fs.writeFile(configPath, userConfig); + + await setupCodexConfig(dir); + const parsed = parseToml(await fs.readFile(configPath, 'utf8')); + assert.equal(parsed.project_doc_max_bytes, 65536); + assert.equal(parsed.features.multi_agent, false, 'user feature value stays unchanged'); + } finally { + await fs.remove(dir); + } +}); + +test('root quota is inserted before unrelated Codex tables', async () => { + const dir = await makeTmpProject(); + try { + const configPath = path.join(dir, '.codex', 'config.toml'); + const userConfig = '[profiles.default]\nmodel = "user-model"\n'; + await fs.ensureDir(path.dirname(configPath)); + await fs.writeFile(configPath, userConfig); + + await setupCodexConfig(dir); + const parsed = parseToml(await fs.readFile(configPath, 'utf8')); + assert.equal(parsed.project_doc_max_bytes, 65536, 'quota remains at the TOML root'); + assert.equal(parsed.features.multi_agent, true); + assert.equal(parsed.profiles.default.model, 'user-model', 'unrelated table is preserved'); + } finally { + await fs.remove(dir); + } +}); + +test('Claude agent output keeps native fields and drops Codex-only modelIntent', async () => { + const dir = await makeTmpProject(); + try { + await install({ target: dir, ide: 'claude-code', quick: true }); + const raw = await fs.readFile(path.join(dir, '.claude', 'agents', 'ui-builder.md'), 'utf8'); + const { fm } = parseSkillFrontmatter(raw); + assert.equal(fm.name, 'ui-builder'); + assert.equal(fm.model, 'sonnet'); + assert.equal(fm.modelIntent, undefined); + } finally { + await fs.remove(dir); + } +}); + test('existing AGENTS.md user content is preserved', async () => { const dir = await makeTmpProject(); try { @@ -168,6 +295,37 @@ test('reinstall is idempotent and deterministic', async () => { } }); +test('native agents preserve user and ZCode files and modified generated agents', async () => { + const dir = await makeTmpProject(); + try { + const userAgentPath = path.join(dir, CODEX_AGENTS_DIR, 'user-agent.toml'); + const zcodePath = path.join(dir, '.zcode', 'agents', 'user.toml'); + const userAgent = 'name = "user-agent"\ndescription = "User-owned"\n'; + await fs.ensureDir(path.dirname(userAgentPath)); + await fs.ensureDir(path.dirname(zcodePath)); + await fs.writeFile(userAgentPath, userAgent); + await fs.writeFile(zcodePath, 'user zcode configuration\n'); + + await quickInstallCodex(dir); + const generatedPath = path.join(dir, CODEX_AGENTS_DIR, 'ui-builder.toml'); + const modified = `${await fs.readFile(generatedPath, 'utf8')}\nUser customization.\n`; + await fs.writeFile(generatedPath, modified); + + await quickInstallCodex(dir); + assert.equal(await fs.readFile(generatedPath, 'utf8'), modified, 'modified generated agent is preserved'); + assert.equal(await fs.readFile(userAgentPath, 'utf8'), userAgent, 'user Codex agent is preserved'); + assert.equal(await fs.readFile(zcodePath, 'utf8'), 'user zcode configuration\n', 'ZCode files are untouched'); + + const result = await uninstallCodex(dir, { backup: false }); + assert.equal(result.removedAgents.length, 7, 'only unmodified generated agents are removed'); + assert.ok(await fs.pathExists(generatedPath), 'modified generated agent remains'); + assert.ok(await fs.pathExists(userAgentPath), 'user Codex agent remains'); + assert.ok(await fs.pathExists(zcodePath), 'ZCode file remains'); + } finally { + await fs.remove(dir); + } +}); + test('unrelated user Codex skills are never touched', async () => { const dir = await makeTmpProject(); try { @@ -209,6 +367,9 @@ test('stale TOH-managed skills are removed on reinstall', async () => { path.join(staleDir, 'SKILL.md'), staleContent ); + const staleAgentPath = path.join(dir, CODEX_AGENTS_DIR, 'toh-legacy-agent.toml'); + const staleAgentContent = 'name = "toh-legacy-agent"\ndescription = "old"\ndeveloper_instructions = "old"\n'; + await fs.writeFile(staleAgentPath, staleAgentContent); const manifestPath = path.join(dir, '.codex', 'toh-framework.json'); const manifest = await fs.readJson(manifestPath); manifest.files['.agents/skills/toh-legacy/SKILL.md'] = { @@ -216,11 +377,17 @@ test('stale TOH-managed skills are removed on reinstall', async () => { kind: 'command', source: '.toh/commands/toh-legacy.md' }; + manifest.agents['.codex/agents/toh-legacy-agent.toml'] = { + sha256: createHash('sha256').update(staleAgentContent).digest('hex'), + source: '.toh/agents/toh-legacy-agent.md', + modelIntent: 'implementation' + }; await fs.writeJson(manifestPath, manifest, { spaces: 2 }); await setupCodex(dir, SRC_DIR, 'en'); assert.ok(!(await fs.pathExists(staleDir)), 'stale TOH-managed skill removed'); + assert.ok(!(await fs.pathExists(staleAgentPath)), 'stale TOH-managed agent removed'); assert.ok(await fs.pathExists(path.join(dir, CODEX_SKILLS_DIR, 'toh-plan', 'SKILL.md')), 'current skills intact'); } finally { await fs.remove(dir); @@ -241,14 +408,16 @@ test('uninstall removes TOH Codex files and keeps user files + .toh state', asyn // Pretend the loop ran: live state that must survive a codex uninstall. await fs.writeFile(path.join(dir, '.toh', 'plan.md'), '# Plan: real work\n\n- [ ] T001 [P] ui-builder — thing in app/page.tsx\n'); - const { removedSkills, agentsMd, config, backupPath } = await uninstallCodex(dir); + const { removedSkills, removedAgents, agentsMd, config, backupPath } = await uninstallCodex(dir); - assert.equal(removedSkills.length, 37, 'all TOH wrappers removed'); + assert.equal(removedSkills.length, 14, 'all TOH workflow wrappers removed'); + assert.equal(removedAgents.length, 8, 'all unmodified native agents removed'); for (const skill of EXPECTED_COMMANDS) { assert.ok(!(await fs.pathExists(path.join(dir, CODEX_SKILLS_DIR, skill))), `removed ${skill}`); } assert.ok(await fs.pathExists(path.join(userSkillDir, 'SKILL.md')), 'user skill remains'); assert.ok(await fs.pathExists(path.join(dir, CODEX_SKILLS_DIR)), '.agents/skills dir kept'); + assert.ok(!(await fs.pathExists(path.join(dir, CODEX_AGENTS_DIR))), 'empty native agent directory removed'); assert.equal(config, 'removed'); assert.ok(backupPath && await fs.pathExists(backupPath), 'uninstall creates a backup'); @@ -285,7 +454,7 @@ test('every generated SKILL.md is valid and its references resolve', async () => const skillsRoot = path.join(dir, CODEX_SKILLS_DIR); const entries = (await fs.readdir(skillsRoot, { withFileTypes: true })).filter((e) => e.isDirectory()); - assert.equal(entries.length, 37, 'exactly 37 TOH wrappers exist'); + assert.equal(entries.length, 14, 'exactly 14 TOH workflow wrappers exist'); const NAME_RE = /^[a-z0-9-]{1,64}$/; for (const entry of entries) { @@ -387,7 +556,8 @@ test('uninstall dry-run reports owned files without changing the project', async const result = await uninstallCodex(dir, { dryRun: true }); assert.equal(result.dryRun, true); - assert.equal(result.removedSkills.length, 37); + assert.equal(result.removedSkills.length, 14); + assert.equal(result.removedAgents.length, 8); assert.equal(result.agentsMd, 'removed'); assert.equal(result.config, 'removed'); assert.deepEqual([...before.keys()].sort(), [...(await snapshotTree(dir)).keys()].sort(), 'dry-run keeps file set'); From 2ebdf5cae2c7e929f7972327187cbc429a50ffb1 Mon Sep 17 00:00:00 2001 From: wasintoh Date: Wed, 2 Sep 2026 23:28:59 +0700 Subject: [PATCH 4/4] feat(codex): native Codex agents in .codex/agents/*.toml, reshaped in review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keeps the heart of PR #3 — one project-scoped Codex custom agent per Toh agent, generated from .toh/agents/, plus the repo's first test suite — and rebuilds it on top of the v2.1.1 codex.js so nothing that shipped there is lost (Thai AGENTS.md, memory templates, config-once, the two-reader AGENTS.md design, the capability probe). What changed against the PR as submitted: - Agent TOML carries NO `model` key. It inherits the parent session's model (per the subagents doc), so one config choice governs every agent and a future model rename never strands an install. Only model_reasoning_effort is set, from the new `modelIntent` frontmatter (lightweight | implementation | planning | review; Claude tier is the fallback). Verified live on codex-cli 0.145.0 with the session on gpt-5.6-sol: ui-builder spawned, ran read-only, returned through the Toh announce contract. - .agents/skills/ keeps a single writer (shared.js, untouched). Installing Cursor first and Codex later now yields 8/8 agents instead of "0/14 workflows" behind a green tick, and Cursor never reads Codex-specific text. - An existing user .codex/config.toml is never modified (byte-identical on install and reinstall); [features] multi_agent is on by default upstream. - "Codex (CLI + desktop app)" naming restored everywhere — subagents and skills work in the Codex app too; the v2.1.0/v2.1.1 CHANGELOG entries keep their original wording and the new work lands under [Unreleased]. - Codex capability profile stays the probed floor; the hard-budget abort is thrown (with the same explanation) instead of process.exit so the CLI sets the exit code and tests can observe it. - AGENTS.md tells Codex to invoke workflows with `$toh-` and to hand a custom agent a self-contained brief (a full-history fork is refused — seen in the live run). - `toh uninstall --ide codex` removes only the hash-verified native agent files plus their manifest, previews and asks first, backs up to .toh-uninstall-backup/; AGENTS.md and config.toml are shared surfaces and stay. The full uninstall knows the new paths. - tests/codex.test.js rewritten: 14 checks, every one an observable outcome of a real install into a temp dir (layout, TOML shape, single-writer invariant across IDE order, config untouched, ownership by hash, AGENTS.md idempotency and budget, both uninstall paths). `npm test` runs in CI and in the release gate. Co-authored-by: Patipat Chewprecha Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Dst2TdqQxdKu3oP3EVbWVN --- .github/workflows/ci.yml | 1 + .github/workflows/release.yml | 1 + CHANGELOG.md | 11 +- CLAUDE.md | 21 +- README.md | 41 +- bin/toh-cli.js | 12 +- docs/README-TH.md | 41 +- installer/ide-handlers/codex.js | 1498 +++++++++++++------- installer/ide-handlers/shared.js | 50 +- installer/install.js | 96 +- installer/uninstall.js | 97 +- package.json | 2 +- src/agents/README.md | 8 +- src/commands/toh-help.md | 4 +- src/memory/MEMORY-SYSTEM.md | 2 +- src/skills/orchestration-protocol/SKILL.md | 11 +- src/skills/progress-tracking/SKILL.md | 2 +- src/skills/smart-routing/SKILL.md | 3 +- tests/codex.test.js | 696 ++++----- 19 files changed, 1428 insertions(+), 1169 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0347fa2..a5eaa3c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,6 +28,7 @@ jobs: - run: npm ci - run: node bin/toh-cli.js --version - run: node bin/toh-cli.js --help + - run: npm test - run: npm run list - run: npm run status - run: npm pack --dry-run diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dbcac6f..0d07177 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -42,6 +42,7 @@ jobs: - run: npm ci - run: node bin/toh-cli.js --version - run: node bin/toh-cli.js --help + - run: npm test - run: npm run list - run: npm pack --dry-run diff --git a/CHANGELOG.md b/CHANGELOG.md index 4436bdc..9ae41e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to Toh Framework will be documented in this file. +## [Unreleased] + +#### Added + +- **Native Codex agents** — every Toh agent in `.toh/agents/` is now also installed as a project-scoped Codex custom agent in `.codex/agents/.toml` (`name`, `description`, `model_reasoning_effort`, `sandbox_mode`, `developer_instructions`), so Codex can delegate to `ui-builder`, `plan-orchestrator` and friends natively. The files deliberately carry **no `model` key**: they inherit the parent session's model, so one config choice governs every agent and a future model rename never strands an install. Reasoning effort comes from a new `modelIntent` frontmatter key (`lightweight | implementation | planning | review`, falling back to the Claude tier), and agents whose tool allowlist has no write tool get Codex's `read-only` sandbox. Ownership is tracked by sha256 in `.codex/toh-framework.json`: a file you edited or created is never overwritten or removed. AGENTS.md now points Codex at `$toh-` skill invocation and the native agents. Contributed by @pcbimon in [PR #3](https://github.com/wasintoh/toh-framework/pull/3); reshaped in review so `.agents/skills/` keeps a single writer (shared.js), an existing `.codex/config.toml` is still never modified, and the codex capability profile stays the probed v2.1.1 floor. +- **`toh uninstall --ide codex`** — removes just the native agent files this installer wrote (hash-verified, backed up first) and their manifest; AGENTS.md, `.codex/config.toml` and `.toh/` stay. The full uninstall also knows the new paths. +- **Test suite** — `npm test` runs `tests/codex.test.js` (node:test, in-band): install layout, TOML shape, single-writer invariant across IDE order, config.toml untouched, ownership by hash, AGENTS.md idempotency and budget, both uninstall paths. First automated tests in the repo; CI now runs them. + ## [2.1.1] - 2026-08-26 ### 🩹 Patch: Updates That Respect Your Work @@ -13,7 +21,6 @@ v2.1.1 is a repair release — อัปเดตได้โดยไม่ท - **Memory survives an update** — re-running the installer over an existing project no longer resets `.toh/memory/`. All 7 memory files are now seeded only if absent, the same contract `.toh/plan.md` and `.toh/progress.md` have had since v2.0.0 — an update never clobbers what the loop has learned. Applied at every one of the 6 inline template code sites (`install.js` + 5 IDE handlers; zcode deliberately has none). - **Stop hooks respect a parked plan** — a plan whose header says `Status: blocked` or `Status: paused` is now terminal for the Claude Code prompt Stop hook, exactly like `Status: done`/`Status: draft`: the loop no longer refuses to end a session over work the user deliberately put down. The deterministic Antigravity command-script hook gains the same exemptions plus `Status: done` (which its grep previously never checked). Existing installs are **upgraded in place**: the installer recognises its own `` entry and rewrites just that entry to the new text instead of treating "marker present" as "nothing to do" — user hook entries are still never removed or reordered. - **Codex capabilities probed, not assumed** — the Codex handler now runs a runtime capability probe and falls back conservatively when the probe cannot confirm a feature, instead of hardcoding what the currently-installed Codex is presumed to support. An unverifiable capability is declared absent — the generated text never promises what the runtime was not proven to do. -- **Native Codex CLI workflows restored** — Codex CLI now receives 14 native workflow skill wrappers under `.agents/skills/` and 8 project-scoped agents under `.codex/agents/*.toml`, with model/reasoning routing and read-only sandbox boundaries translated from the canonical agent source. The managed `AGENTS.md` block remains compact and `.toh/` state is preserved on reinstall. - **Version-drift sweep completed** — re-audited every user-visible and generated surface (`src/`, `installer/`, `README.md`, `docs/README-TH.md`, plus a real install of all 5 IDE targets) for stale `Next.js 14` / `React 18` / `Tailwind 3` stack mentions left after 0795eec. Result: zero remaining — generated output states only Next.js 16 / React 19 / Tailwind 4, matching the `src/templates/nextjs-pro/package.json` pins. Historical CHANGELOG entries and archived planning docs keep their original wording on purpose. Fixes [GitHub issue #2](https://github.com/wasintoh/toh-framework/issues/2) — thank you @tumansdev for the detailed report. @@ -83,7 +90,7 @@ v2.1 is a compatibility release — ตรวจจริง ซ่อมจร - `.toh/capabilities.json` profiles updated: cursor `subagents: "native"`; antigravity `subagents: "file-based"`, `hooks: true`, `workflows: true`. Union semantics are additive — projects installed under v2.0 with gemini keep both `gemini-cli` and `antigravity` declared. - Removed `bin/toh-npx-wrapper.js`; `installer/list.js` rewritten to live-read `src/`. Package tarball: 142 files, ~1.1 MB unpacked (`npm pack --dry-run`). - New `installer/uninstall.js`, lazy-loaded by `bin/toh-cli.js` like every other command; `installer/install.js` gained a before/after content snapshot of its own surfaces (`.toh`, `.claude`, `.cursor`, `.agents`, `.agent`, `.codex`, `.gemini`, `CLAUDE.md`, `AGENTS.md`, `.cursorrules`) to build the schema-2 inventory. The snapshot never follows symlinks and never walks the project tree. `generateClaudeMd` / `generateClaudeMdBlock` are now exported from `installer/ide-handlers/claude-code.js` so the uninstaller can recognise our own generated text in pre-2.1 projects. New npm script `uninstall:local`. -- package.json: version **2.0.0 → 2.1.0**; description and keywords now name Claude Code, Cursor, Antigravity (+ Antigravity CLI), Codex CLI, and ZCode — the dead `gemini` keyword dropped; `codex`, `openai-codex`, `antigravity-cli`, `agy`, `agent-skills`, `zcode`, `z-ai` added. +- package.json: version **2.0.0 → 2.1.0**; description and keywords now name Claude Code, Cursor, Antigravity (+ Antigravity CLI), Codex (CLI + desktop app), and ZCode — the dead `gemini` keyword dropped; `codex`, `openai-codex`, `antigravity-cli`, `agy`, `agent-skills`, `zcode`, `z-ai` added. --- diff --git a/CLAUDE.md b/CLAUDE.md index ad099d4..57f39b9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,9 +5,9 @@ Guide for a Claude Code agent developing the framework itself. Repo-only: not in ## What this is -Toh Framework ("Type Once, Have it all") is the npm package `toh-framework` (v2.1.1, MIT, ESM, +Toh Framework ("Type Once, Have it all") is the npm package `toh-framework` (v2.1.0, MIT, ESM, Node >= 18, no build step) that installs an AI-orchestration development system into 6 IDEs: -Claude Code, Cursor (2.4+), Antigravity (agy CLI + IDE), Codex CLI, ZCode (Z.ai), and — +Claude Code, Cursor (2.4+), Antigravity (agy CLI + IDE), Codex (CLI + desktop app), ZCode (Z.ai), and — Enterprise-only, behind `--legacy-gemini` — Gemini CLI (consumer service shut down 2026-06-18). North Star: a non-technical person types one sentence, approves once ("Go"), and THE TOH LOOP @@ -26,13 +26,12 @@ All npm scripts wrap `node bin/toh-cli.js `, which lazy-loads `installer/*. - `npm run list` — print the catalog, live-read from src/ frontmatter (throws on bad YAML) - `npm run status` — inspect install state (~/.claude, ./.toh, manifest.json) - `npm run bundle` — web prompt bundles into ./dist/web-bundles -- `npm test` — node:test suite (tests/, in-band runner; currently covers the Codex installer) - `npm pack --dry-run` — check exactly what ships before any packaging change ## Verification protocol -`npm test` covers the Codex install/uninstall behavior; the rest is verified by running for real -(.github/workflows/ci.yml runs both on Node 18 + 22): +`npm test` runs tests/codex.test.js (node:test, in-band via tests/run.js; TOH_QUIET=1 silences ora) — +it covers the Codex surfaces only. Everything else is still verified by running for real: 1. Run what you touched — install into a scratch dir, `npm run list`/`status`, `npm pack --dry-run`. Inspect the generated output (.toh/, .claude/, .cursor/rules/ + .cursor/agents/, AGENTS.md + @@ -71,6 +70,11 @@ skills converted from the TOML; throws on unparseable sources): - codex.js → one root AGENTS.md between TOH-FRAMEWORK-START/END markers — compact agent roster + indexed command table; bodies read at runtime from .toh/. Hard-asserts the block <=24 KiB (Codex silently truncates at 32 KiB) and emits .codex/config.toml raising project_doc_max_bytes, never overwriting an existing one. + v2.2: also writes one native Codex agent per Toh agent to .codex/agents/.toml — NO `model` + key (inherits the session), `model_reasoning_effort` from the agent's `modelIntent` frontmatter, + `sandbox_mode = "read-only"` when the tool allowlist has no write tool. Ownership by sha256 in + .codex/toh-framework.json: an edited or user-created file is never overwritten or removed. + It never writes .agents/skills/ — shared.js is the single writer for that directory. - zcode.js → thin by design. ZCode reads the same open surfaces, so it writes NO `.zcode/`: it reuses codex.js's exported writeAgentsMd() for AGENTS.md (ide 'zcode' swaps three sentences) and shared.js writeAgentsCommands() for `.agents/commands/` — 14 native `/toh-*` slash commands, the @@ -80,13 +84,6 @@ skills converted from the TOML; throws on unparseable sources): - gemini-cli.js → LEGACY (.gemini/: TOML commands, skills, GEMINI.md, settings.json), only via --legacy-gemini; off the menu; no longer implies Antigravity. `--ide gemini` without the flag warns and substitutes antigravity. -- codex.js → NATIVE Codex CLI workflows: one thin command wrapper per user-facing command at - `.agents/skills//SKILL.md` (generated from `src/commands/`; each wrapper points at - `.toh/commands/*.md` and internal skills stay in `.toh/skills/`) + native custom agents at - `.codex/agents/.toml` (generated from `src/agents/`, with centralized model/reasoning - routing and read-only sandbox mapping) + a concise managed `AGENTS.md` block. Never embed - agent bodies in `AGENTS.md` again. `uninstallCodex()` removes only generator-marked Codex - skills/agents and the managed block; `.toh/` runtime is seeded only-if-absent. Per-IDE command divergence lives in ONE markdown source via `` (kept only for Claude Code) / `` (kept for everyone else) blocks, resolved by shared.js diff --git a/README.md b/README.md index 446b46c..3927970 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Changed your mind? `npx toh-framework uninstall` shows you exactly what it will | 🧠 **Claude Code** | ✅ Full Support | Native subagents + skills preload, Stop hook, slash commands & shortcuts | | 📝 **Cursor (2.4+)** | ✅ Full Support | Native subagents (`.cursor/agents/`), skills via `.agents/skills/`, always-on rules | | 🛰️ **Antigravity CLI (agy) + IDE** | ✅ Full Support | `.agents/` rules + skills + workflows + subagents + Stop hook | -| 🤖 **Codex CLI** | ✅ Supported | Native workflow skills + project agents (`.codex/agents/`) | +| 🤖 **Codex** — CLI + Codex desktop app (ChatGPT app) | ✅ Supported | Compact AGENTS.md + repo-level skills + native agents in `.codex/agents/` | | 💠 **ZCode (Z.ai)** | ✅ Supported | AGENTS.md + `.agents/skills/` + native `/toh-*` in `.agents/commands/` | | 💎 **Gemini CLI** | 🏢 Legacy | Enterprise/GCP only, behind `--legacy-gemini` | @@ -231,42 +231,21 @@ agy /toh-vibe Inventory management system ``` -### Codex CLI - -Codex has no custom slash commands, so TOH installs **native Codex workflow -skills** under `.agents/skills/` — 14 top-level `$toh-*` workflows. The 23 -supporting skills stay in `.toh/skills/` so the global skill list remains focused. -TOH also generates eight project-scoped native agents under `.codex/agents/*.toml`. -Codex CLI uses native project files, including native model and reasoning -routing for each agent. `.codex/config.toml` enables multi-agent -execution when the project does not already define its own `[features]` table. +### Codex — CLI and Codex desktop app (ChatGPT app) ```bash -# Open the project root in Codex CLI codex -# Invoke a TOH skill explicitly ($ + skill name), or browse with /skills -$toh-vibe coffee shop management system -$toh-plan build a booking app with payments +# Invoke a Toh workflow as a native Codex skill ($ + name), or browse with /skills +$toh-vibe Inventory management system -# Or just describe the task — Codex matches the skill by its description -"Create an inventory management system" +# Plain /toh-* text works too — AGENTS.md teaches Codex the full command set +/toh-vibe Inventory management system ``` -TOH stores framework state under `.toh/` (`plan.md`, `progress.md`, -`memory/`), native workflow discovery under `.agents/skills/`, and native agent -definitions under `.codex/agents/`. Project-level rules live in the managed block -of `AGENTS.md`. For compatibility, typing `/toh-vibe ...` as plain text is -interpreted (via `AGENTS.md`) as a request for the matching skill — but it is -**not** a native Codex slash command. TOH does not read or modify ZCode files or -global Codex configuration. - -Uninstall (removes only TOH-managed Codex files; your own skills and -agents, ZCode files, and `AGENTS.md` text are kept): - -```bash -npx toh-framework uninstall --ide codex -``` +The 8 Toh agents are also installed as native Codex agents in `.codex/agents/*.toml` +(generated from `.toh/agents/`, ownership-tracked so your edits are never overwritten). +They inherit the model of your session and carry only a reasoning-effort hint per role. ### ZCode (Z.ai) @@ -402,7 +381,7 @@ claude -p "/toh-vibe coffee shop management system" --permission-mode acceptEdit - 📚 **23 Skills** - Comprehensive AI capabilities, shipped once to `.agents/skills/` for every IDE that reads the open standard `[NEW in 2.1]` - 🎨 **Design Identity** - Per-project DESIGN.md design identity + versioned AVOID-LIST - 📦 **15 Component Templates** - Ready-to-use premium components -- 🌐 **6 IDEs** - Claude Code, Cursor, Antigravity (+ Antigravity CLI), Codex CLI, ZCode, Gemini CLI (legacy) +- 🌐 **6 IDEs** - Claude Code, Cursor, Antigravity (+ Antigravity CLI), Codex (CLI + desktop app), ZCode, Gemini CLI (legacy) --- diff --git a/bin/toh-cli.js b/bin/toh-cli.js index 9f15565..b716d48 100755 --- a/bin/toh-cli.js +++ b/bin/toh-cli.js @@ -55,7 +55,13 @@ program .option('--legacy-cursorrules', 'Also write the legacy root .cursorrules file (very old Cursor versions)') .action(async (options) => { const { install } = await import('../installer/install.js'); - await install(options); + try { + await install(options); + } catch (error) { + // Hard-budget aborts already printed their explanation (error.reported). + if (!error.reported) console.error(chalk.red(`\n✖ Installation failed: ${error.message}\n`)); + process.exit(1); + } }); // Uninstall command — the counterpart to install. @@ -70,8 +76,8 @@ program .option('-y, --yes', 'Skip the confirmation question (for scripts)') .option('--all', 'ALSO delete your own plan, work log and project notes (a backup copy is saved first)') .option('--verbose', 'List every file in the preview instead of a per-tool summary') - .option('-i, --ide ', 'IDE to remove (currently: codex). Omit for full uninstall') - .option('--no-backup', 'Do not create a backup before Codex-only removal') + .option('-i, --ide ', 'Remove one IDE surface only (currently: codex = the native .codex/agents/ files). Omit for the full uninstall') + .option('--no-backup', 'With --ide: skip the backup copy of removed files') .action(async (options) => { const { uninstall } = await import('../installer/uninstall.js'); const code = await uninstall(options); diff --git a/docs/README-TH.md b/docs/README-TH.md index 9f45dfd..892e1d1 100644 --- a/docs/README-TH.md +++ b/docs/README-TH.md @@ -63,7 +63,7 @@ AI จะเขียนแผนออกมาเป็นรายการ | 🧠 **Claude Code** | ✅ รองรับเต็ม | Native subagents + skills preload, Stop hook, slash commands และทางลัด | | 📝 **Cursor (2.4+)** | ✅ รองรับเต็ม | Native subagents (`.cursor/agents/`), skills ผ่าน `.agents/skills/`, rule แบบ always-on | | 🛰️ **Antigravity CLI (agy) + IDE** | ✅ รองรับเต็ม | `.agents/` rules + skills + workflows + subagents + Stop hook | -| 🤖 **Codex CLI** | ✅ รองรับ | Native workflow skills + project agents (`.codex/agents/`) | +| 🤖 **Codex** (CLI + Codex desktop app / ChatGPT app) | ✅ รองรับ | AGENTS.md แบบกะทัดรัด + repo-level skills + native agents ใน `.codex/agents/` | | 💠 **ZCode (Z.ai)** | ✅ รองรับ | AGENTS.md + `.agents/skills/` + คำสั่ง `/toh-*` native ใน `.agents/commands/` | | 💎 **Gemini CLI** | 🏢 Legacy | เฉพาะ Enterprise/GCP ใช้ผ่าน `--legacy-gemini` | @@ -238,42 +238,21 @@ agy /toh-vibe ระบบจัดการ inventory ``` -### Codex CLI - -Codex ไม่มี slash command แบบกำหนดเอง ดังนั้น TOH จะติดตั้ง **native Codex -workflow skills** ไว้ที่ `.agents/skills/` — workflow ระดับบน 14 ตัวในรูปแบบ -`$toh-*` ส่วน supporting skills 23 ตัวจะอยู่ใน `.toh/skills/` เพื่อให้รายการ -skill หลักกระชับ นอกจากนี้ TOH สร้าง native agents แบบ project-scoped 8 ตัวไว้ที่ -`.codex/agents/*.toml` โดย Codex CLI ใช้ไฟล์โปรเจคชุดเดียวกัน รวมถึงการกำหนด -model และ reasoning ของแต่ละ agent โดย `.codex/config.toml` จะเปิด -multi-agent ให้เมื่อโปรเจคยังไม่มี `[features]` ของตัวเอง +### Codex (CLI และ Codex desktop app / ChatGPT app) ```bash -# เปิดโฟลเดอร์โปรเจคใน Codex CLI codex -# เรียก skill ตรงๆ ด้วย $ + ชื่อ skill (หรือพิมพ์ /skills เพื่อดูทั้งหมด) -$toh-vibe ระบบจัดการร้านกาแฟ -$toh-plan สร้างแอปจองห้องพร้อมชำระเงิน +# เรียก workflow ของ Toh เป็น skill ของ Codex ตรงๆ ด้วย $ ตามด้วยชื่อ หรือพิมพ์ /skills ดูทั้งหมด +$toh-vibe ระบบจัดการ inventory -# หรือแค่บรรยายงาน — Codex จะเลือก skill จาก description ให้เอง -"สร้างระบบจัดการ inventory" +# พิมพ์ /toh-* แบบเดิมก็ยังใช้ได้ AGENTS.md สอนชุดคำสั่งครบให้ Codex อยู่แล้ว +/toh-vibe ระบบจัดการ inventory ``` -TOH เก็บ state ของ framework ไว้ที่ `.toh/` (`plan.md`, `progress.md`, -`memory/`), workflow discovery ไว้ที่ `.agents/skills/` และ native agent -definitions ไว้ที่ `.codex/agents/` กฎระดับโปรเจคอยู่ใน block ที่ TOH จัดการของ -`AGENTS.md` เพื่อความเข้ากันได้แบบเดิม ถ้าพิมพ์ `/toh-vibe ...` เป็นข้อความธรรมดา -ระบบจะตีความ (ผ่าน `AGENTS.md`) ว่าเป็นการเรียก skill ที่ตรงกัน — แต่มัน -**ไม่ใช่** native slash command ของ Codex TOH จะไม่อ่านหรือแก้ไขไฟล์ ZCode หรือ -global Codex configuration - -ถอนการติดตั้ง (ลบเฉพาะไฟล์ Codex ที่ TOH สร้าง — skill ของคุณเองและข้อความ -agent ของคุณ ไฟล์ ZCode และข้อความใน `AGENTS.md` จะถูกเก็บไว้): - -```bash -npx toh-framework uninstall --ide codex -``` +agent ทั้ง 8 ตัวของ Toh ถูกติดตั้งเป็น native agent ของ Codex ด้วย อยู่ที่ `.codex/agents/*.toml` +สร้างจาก `.toh/agents/` และจำไว้ว่าไฟล์ไหนเป็นของเรา ไฟล์ที่คุณแก้เองจะไม่ถูกเขียนทับ +ทุกตัวใช้ model เดียวกับ session ของคุณ มีแค่ระดับ reasoning ที่ตั้งไว้ตามหน้าที่ของแต่ละตัว ### ZCode (Z.ai) @@ -409,7 +388,7 @@ claude -p "/toh-vibe ระบบจัดการร้านกาแฟ" --p - 📚 **23 Skills** - ความสามารถของ AI ครบชุด ส่งครั้งเดียวลง `.agents/skills/` ให้ทุก IDE ที่อ่านมาตรฐานเปิดนี้ `[ใหม่ใน 2.1]` - 🎨 **Design Identity** - DESIGN.md ประจำโปรเจค คู่กับ AVOID-LIST ที่มีเวอร์ชัน - 📦 **15 Component Templates** - component ระดับพรีเมียม หยิบไปใช้ได้เลย -- 🌐 **6 IDEs** - Claude Code, Cursor, Antigravity (+ Antigravity CLI), Codex CLI, ZCode, Gemini CLI (legacy) +- 🌐 **6 IDEs** - Claude Code, Cursor, Antigravity (+ Antigravity CLI), Codex (CLI + desktop app), ZCode, Gemini CLI (legacy) --- diff --git a/installer/ide-handlers/codex.js b/installer/ide-handlers/codex.js index d71606d..85c450f 100644 --- a/installer/ide-handlers/codex.js +++ b/installer/ide-handlers/codex.js @@ -1,72 +1,105 @@ /** - * Native Codex CLI installer surfaces. - * - * Shared Toh runtime files stay under .toh/. Codex receives workflow skills in - * .agents/skills and project-scoped native agents in .codex/agents. + * Codex IDE Handler (CLI + desktop app) + * Creates AGENTS.md (project memory, auto-loaded by Codex) and, since v2.2, + * one native Codex agent per Toh agent in .codex/agents/*.toml. + * The 14 /toh-* command skills and 23 framework skills reach Codex through + * the shared .agents/skills/ writer in shared.js — this file never writes there. */ -import crypto from 'crypto'; import fs from 'fs-extra'; import path from 'path'; import { fileURLToPath } from 'url'; import yaml from 'js-yaml'; -import { parse as parseToml } from 'smol-toml'; -import { renderCapabilitiesSection, seedFileIfAbsent, transformCommand } from './shared.js'; +import { transformCommand, renderCapabilitiesSection, seedFileIfAbsent } from './shared.js'; import { probeCodexCapabilitiesCached } from './capability-probe.js'; +import crypto from 'crypto'; +import { parse as parseToml } from 'smol-toml'; + +// Hard budget for the TOH marker block inside AGENTS.md. Codex silently +// truncates project docs at 32 KiB COMBINED (project_doc_max_bytes default), +// including any pre-existing user content above our marker — so our block must +// stay well under that. Exceeding this is a build bug, never a warning. +const MAX_TOH_BLOCK_BYTES = 24 * 1024; + +// AGENTS.md is a shared open surface: Codex reads it as project memory, and so +// does ZCode (Z.ai) — same filename, same location, same marker block. Only +// three sentences differ per runtime, so both handlers build from one generator. +// Keys must match the canonical IDE keys in shared.js CAPABILITY_PROFILES. +const AGENTS_MD_RUNTIMES = { + codex: { + memoryEN: 'This file serves as project memory for Codex (CLI and desktop app). It contains the Toh Framework configuration and agent definitions.', + memoryTH: 'This file is project memory for Codex (CLI and desktop app) containing Toh Framework configuration and agent definitions', + runtimeName: 'Codex', + commandHint: ' The 14 `/toh-*` workflows are also installed as Codex skills in `.agents/skills/` — invoke one explicitly with `$toh-` (e.g. `$toh-vibe`) or browse them with `/skills`; typing `/toh-vibe ...` as plain text works too. The 8 Toh agents are installed as native Codex agents in `.codex/agents/*.toml` (full specs stay in `.toh/agents/`); when you delegate to one, hand it a self-contained brief — custom agents cannot receive a full-history fork.' + }, + zcode: { + memoryEN: 'This file serves as project memory for ZCode (Z.ai). It contains the Toh Framework configuration and agent definitions.', + memoryTH: 'This file is project memory for ZCode (Z.ai) containing Toh Framework configuration and agent definitions', + runtimeName: 'ZCode', + commandHint: ' The 14 `/toh-*` commands are installed natively in `.agents/commands/` — invoke them directly; the same prompts are also discoverable as skills in `.agents/skills/`.' + } +}; +/** + * The 'Runtime Identity' paragraph shared by the EN and TH generators. + * + * AGENTS.md is ONE file with TWO readers (Codex and ZCode) — it must never + * claim a capability only one reader has. `probedSubagents` is therefore true + * ONLY when codex is the sole writer of AGENTS.md for this run (no ZCode + * selected now or declared earlier) AND the install-time capability probe + * verified codex subagents as stable+enabled. In every other case the + * conservative sentence below is byte-identical to pre-probe releases. + */ +function runtimeIdentityLine(rt, probedSubagents = false) { + const capabilityClause = probedSubagents + ? 'Native subagents: available per probed codex features — delegate for independent tasks; otherwise run the TOH LOOP sequentially' + : 'Multi-agent features (subagents/teams) are unavailable here — execute the TOH LOOP sequentially'; + return `Runtime Identity: you are running in ${rt.runtimeName}. ${capabilityClause} in this session: implement -> run the story's checkpoint -> quote the actual output -> fix if red (max 5 tries, 3 consecutive failures = mark [!] BLOCKED and move on) -> tick the checkbox -> next story WITHOUT asking. Interrupted runs resume at the first unchecked box in .toh/plan.md — unless its header carries a terminal status (Status: done/draft/blocked/paused): a terminal plan is reported, never auto-resumed. Close every stage with the engineer-harness announce contract (Status/Result/Evidence/exactly 3 next actions).${rt.commandHint}`; +} + +// Read version from package.json const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '../../package.json'), 'utf8')); +const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '../../package.json'), 'utf-8')); const VERSION = pkg.version; -export const CODEX_SKILLS_DIR = path.join('.agents', 'skills'); +// --------------------------------------------------------------------------- +// Native Codex agents (v2.2 — contributed by @pcbimon in PR #3, reshaped in +// review). Codex discovers project-scoped custom agents in .codex/agents/*.toml +// (developers.openai.com/codex/subagents); each Toh agent in .toh/agents/.md +// becomes one TOML file. Two deliberate choices: +// 1. NO `model` key. An agent file without `model` inherits the parent +// session's model (per the subagents doc), so the user's one config choice +// governs every agent and a future model rename never strands an install. +// Only `model_reasoning_effort` is set, from the agent's declared intent. +// 2. Ownership by hash. .codex/toh-framework.json records the sha256 of every +// agent file we wrote. A file whose hash no longer matches was edited (or +// created) by the user and is never overwritten or removed. +// --------------------------------------------------------------------------- export const CODEX_AGENTS_DIR = path.join('.codex', 'agents'); -export const AGENTS_MAX_BYTES = 24 * 1024; -export const CODEX_PROJECT_DOC_MAX_BYTES = 64 * 1024; - -const AGENTS_BLOCK_START = ''; -const AGENTS_BLOCK_END = ''; -const AGENTS_BLOCK_RE = /[ \t]*[\s\S]*?[ \t]*\r?\n?/g; -const CONFIG_BLOCK_START = '# TOH-FRAMEWORK-START'; -const CONFIG_BLOCK_END = '# TOH-FRAMEWORK-END'; -const CONFIG_BLOCK_RE = /[ \t]*# TOH-FRAMEWORK-START\r?\n[\s\S]*?[ \t]*# TOH-FRAMEWORK-END[ \t]*\r?\n?/g; -const SKILL_GENERATOR = 'toh-framework'; -const MANIFEST_PATH = path.join('.codex', 'toh-framework.json'); -const SKILL_NAME_RE = /^[a-z0-9-]{1,64}$/; +export const CODEX_MANIFEST_PATH = path.join('.codex', 'toh-framework.json'); +const MANIFEST_GENERATOR = 'toh-framework'; +const AGENT_NAME_RE = /^[a-z0-9-]{1,64}$/; const MODEL_INTENTS = new Set(['lightweight', 'implementation', 'planning', 'review']); +// Same rule cursor.js uses for `readonly`: an allowlist with no write tool is +// read-only by design (root-cause-debugger: Read/Grep/Glob/Bash). const READ_ONLY_TOOLS = new Set(['Read', 'Grep', 'Glob', 'Bash']); -export const CODEX_MODEL_ROUTING = Object.freeze({ - lightweight: Object.freeze({ model: 'gpt-5.6-luna', model_reasoning_effort: 'low' }), - implementation: Object.freeze({ model: 'gpt-5.6', model_reasoning_effort: 'medium' }), - planning: Object.freeze({ model: 'gpt-5.6', model_reasoning_effort: 'high' }), - review: Object.freeze({ model: 'gpt-5.6-terra', model_reasoning_effort: 'high' }) +/** Toh model intent → Codex reasoning effort. The model itself is inherited. */ +export const CODEX_REASONING_EFFORT = Object.freeze({ + lightweight: 'low', + implementation: 'medium', + planning: 'high', + review: 'high' }); function sha256(value) { return crypto.createHash('sha256').update(value).digest('hex'); } -function relativePath(...parts) { - return path.posix.join(...parts.map((part) => String(part).replaceAll(path.sep, '/'))); -} - -function skillFileRelativePath(name) { - return relativePath(CODEX_SKILLS_DIR, name, 'SKILL.md'); -} - -function agentFileRelativePath(name) { - return relativePath(CODEX_AGENTS_DIR, `${name}.toml`); -} - -function parseFrontmatterDocument(raw, label) { - const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/); - if (!match) throw new Error(`${label} must start with YAML frontmatter.`); - try { - return { frontmatter: yaml.load(match[1]) || {}, body: match[2] }; - } catch (error) { - throw new Error(`Invalid YAML frontmatter in ${label}: ${error.message}`); - } +function agentRelPath(name) { + // Manifest keys are POSIX so the file is portable across platforms. + return `.codex/agents/${name}.toml`; } function normalizeModelIntent(value) { @@ -82,51 +115,30 @@ function normalizeModelIntent(value) { return aliases[intent] || intent; } -export function resolveCodexModelIntent(agent) { - const explicit = normalizeModelIntent(agent.modelIntent || agent.model_intent || agent.codex?.modelIntent); +/** + * `modelIntent` frontmatter wins; otherwise derive from the Claude tier so + * agents that predate the key still get sensible reasoning effort. + */ +export function resolveCodexModelIntent(frontmatter = {}) { + const explicit = normalizeModelIntent(frontmatter.modelIntent || frontmatter.model_intent); if (MODEL_INTENTS.has(explicit)) return explicit; - const legacyModel = String(agent.model || '').trim().toLowerCase(); - if (legacyModel === 'haiku') return 'lightweight'; - if (legacyModel === 'opus') return 'planning'; + const tier = String(frontmatter.model || '').trim().toLowerCase(); + if (tier === 'haiku') return 'lightweight'; + if (tier === 'opus') return 'planning'; return 'implementation'; } -async function readManifest(targetDir) { - const manifestPath = path.join(targetDir, MANIFEST_PATH); - if (!(await fs.pathExists(manifestPath))) { - return { generator: SKILL_GENERATOR, version: VERSION, files: {}, agents: {} }; - } - try { - const manifest = await fs.readJson(manifestPath); - if (manifest.generator !== SKILL_GENERATOR || typeof manifest.files !== 'object') { - return { generator: SKILL_GENERATOR, version: VERSION, files: {}, agents: {} }; - } - return { agents: {}, ...manifest }; - } catch { - return { generator: SKILL_GENERATOR, version: VERSION, files: {}, agents: {} }; - } -} - -async function writeManifest(targetDir, manifest) { - const manifestPath = path.join(targetDir, MANIFEST_PATH); - await fs.ensureDir(path.dirname(manifestPath)); - await fs.writeJson(manifestPath, manifest, { spaces: 2 }); -} - -async function updateManifest(targetDir, changes) { - const manifest = await readManifest(targetDir); - await writeManifest(targetDir, { ...manifest, ...changes }); -} - -async function fileMatchesHash(filePath, expectedHash) { - if (!expectedHash || !(await fs.pathExists(filePath))) return false; +function parseAgentFile(raw, label) { + const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/); + if (!match) throw new Error(`[toh-framework] ${label} must start with YAML frontmatter.`); try { - return sha256(await fs.readFile(filePath)) === expectedHash; - } catch { - return false; + return { frontmatter: yaml.load(match[1]) || {}, body: match[2] }; + } catch (error) { + throw new Error(`[toh-framework] Invalid YAML frontmatter in ${label}: ${error.message}`); } } +/** Read the installed Toh agents from .toh/agents/ (the runtime source of truth). */ export async function readAgentCatalog(targetDir) { const agentsDir = path.join(targetDir, '.toh', 'agents'); if (!(await fs.pathExists(agentsDir))) return []; @@ -137,12 +149,12 @@ export async function readAgentCatalog(targetDir) { const agents = []; for (const file of files) { const sourcePath = path.join(agentsDir, file); - const { frontmatter, body } = parseFrontmatterDocument(await fs.readFile(sourcePath, 'utf8'), sourcePath); + const { frontmatter, body } = parseAgentFile(await fs.readFile(sourcePath, 'utf-8'), sourcePath); const name = String(frontmatter.name || file.replace(/\.md$/, '')).trim(); - if (!SKILL_NAME_RE.test(name)) continue; + if (!AGENT_NAME_RE.test(name)) continue; agents.push({ name, - description: String(frontmatter.description || `${name} Toh Framework agent`).replace(/\s+/g, ' ').trim().slice(0, 1024), + description: String(frontmatter.description || `${name} (Toh Framework agent)`).replace(/\s+/g, ' ').trim().slice(0, 1024), body, tools: Array.isArray(frontmatter.tools) ? frontmatter.tools.map(String) : [], skills: Array.isArray(frontmatter.skills) ? frontmatter.skills.map(String) : [], @@ -158,27 +170,32 @@ function isReadOnlyAgent(agent) { return agent.tools.length > 0 && agent.tools.every((tool) => READ_ONLY_TOOLS.has(tool)); } +/** One Toh agent → one Codex agent TOML document (validated before it is returned). */ export function translateAgentToCodex(agent) { - const routing = CODEX_MODEL_ROUTING[agent.modelIntent] || CODEX_MODEL_ROUTING.implementation; + const effort = CODEX_REASONING_EFFORT[agent.modelIntent] || CODEX_REASONING_EFFORT.implementation; const skillRefs = agent.skills.length - ? `\nAssociated Toh skills:\n${agent.skills.map((skill) => `- .toh/skills/${skill}/SKILL.md`).join('\n')}` + ? `\nAssociated Toh skills (read before acting):\n${agent.skills.map((skill) => `- .toh/skills/${skill}/SKILL.md`).join('\n')}` : ''; const toolBoundary = agent.tools.length ? `\nSource tool boundary: ${agent.tools.join(', ')}. Do not widen it.` : ''; - const triggerHints = agent.triggers.length - ? `\nRouting hints: ${agent.triggers.join('; ')}` - : ''; - const turnHint = agent.maxTurns === undefined - ? '' - : `\nSource turn budget hint: ${agent.maxTurns}.`; - const instructions = `${agent.body.trim()}\n\n## Codex runtime contract\n- Own only the task and files assigned by the parent.\n- Return Status, Result, Evidence, Files, and Blockers.\n- Run the supplied checkpoint; the parent re-runs it before changing .toh/plan.md.\n- Keep dependent work sequential and return to the parent when complete.${skillRefs}${toolBoundary}${triggerHints}${turnHint}`; + const triggerHints = agent.triggers.length ? `\nRouting hints: ${agent.triggers.join('; ')}` : ''; + const turnHint = agent.maxTurns === undefined ? '' : `\nSource turn budget hint: ${agent.maxTurns}.`; + const instructions = `${agent.body.trim()} + +## Codex runtime contract +- Own only the task and files assigned by the parent. +- Return Status, Result, Evidence, Files, and Blockers. +- Run the supplied checkpoint; the parent re-runs it before changing .toh/plan.md. +- Keep dependent work sequential and return to the parent when complete.${skillRefs}${toolBoundary}${triggerHints}${turnHint}`; + const content = [ - `# Toh model intent: ${agent.modelIntent}`, + `# Generated by Toh Framework v${VERSION} from .toh/agents/${agent.name}.md`, + `# Toh model intent: ${agent.modelIntent}. No \`model\` key on purpose: the agent`, + `# inherits the parent session's model, so your one config choice governs it.`, `name = ${JSON.stringify(agent.name)}`, `description = ${JSON.stringify(agent.description)}`, - `model = ${JSON.stringify(routing.model)}`, - `model_reasoning_effort = ${JSON.stringify(routing.model_reasoning_effort)}`, + `model_reasoning_effort = ${JSON.stringify(effort)}`, `sandbox_mode = ${JSON.stringify(isReadOnlyAgent(agent) ? 'read-only' : 'workspace-write')}`, `developer_instructions = ${JSON.stringify(instructions.trim())}`, '' @@ -186,489 +203,964 @@ export function translateAgentToCodex(agent) { try { parseToml(content); } catch (error) { - throw new Error(`Invalid generated Codex agent TOML for ${agent.name}: ${error.message}`); + throw new Error(`[toh-framework] Generated Codex agent TOML for ${agent.name} is invalid: ${error.message}`); } return content; } -export async function readCommandCatalog(srcDir) { - const commandsDir = path.join(srcDir, 'commands'); - if (!(await fs.pathExists(commandsDir))) { - throw new Error(`TOH command source not found: ${commandsDir} — is this a complete toh-framework package?`); - } - const files = (await fs.readdir(commandsDir)) - .filter((file) => file.endsWith('.md') && file !== 'README.md') - .sort(); - const commands = []; - for (const file of files) { - const sourcePath = path.join(commandsDir, file); - const raw = await fs.readFile(sourcePath, 'utf8'); - const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/); - if (!match) continue; - let frontmatter; - try { - frontmatter = yaml.load(match[1]) || {}; - } catch (error) { - throw new Error(`Invalid YAML frontmatter in src/commands/${file}: ${error.message}`); +async function readCodexManifest(targetDir) { + const manifestPath = path.join(targetDir, CODEX_MANIFEST_PATH); + const empty = { generator: MANIFEST_GENERATOR, version: VERSION, agents: {} }; + if (!(await fs.pathExists(manifestPath))) return empty; + try { + const manifest = await fs.readJson(manifestPath); + if (manifest.generator !== MANIFEST_GENERATOR || typeof manifest.agents !== 'object' || manifest.agents === null) { + return empty; } - const command = String(frontmatter.command || '').trim(); - const skillName = command.replace(/^\//, ''); - if (!SKILL_NAME_RE.test(skillName)) continue; - commands.push({ - kind: 'command', - skillName, - command, - aliases: Array.isArray(frontmatter.aliases) ? frontmatter.aliases.map(String) : [], - description: String(frontmatter.description || '').trim(), - skills: Array.isArray(frontmatter.skills) ? frontmatter.skills.map(String) : [], - file, - sourcePath: `.toh/commands/${file}` - }); - } - if (commands.length === 0) throw new Error(`No TOH commands found in ${commandsDir} — cannot generate Codex skills.`); - return commands; -} - -export async function readSupportingSkillCatalog(srcDir) { - const skillsDir = path.join(srcDir, 'skills'); - if (!(await fs.pathExists(skillsDir))) { - throw new Error(`TOH skill source not found: ${skillsDir} — is this a complete toh-framework package?`); - } - const entries = (await fs.readdir(skillsDir, { withFileTypes: true })) - .filter((entry) => entry.isDirectory()) - .sort((a, b) => a.name.localeCompare(b.name)); - const skills = []; - for (const entry of entries) { - const file = path.join(skillsDir, entry.name, 'SKILL.md'); - if (!(await fs.pathExists(file))) continue; - const raw = await fs.readFile(file, 'utf8'); - const frontmatter = raw.startsWith('---') ? parseFrontmatterDocument(raw, file).frontmatter : {}; - const name = String(frontmatter.name || entry.name).trim(); - if (!SKILL_NAME_RE.test(name) || name !== entry.name) continue; - const heading = raw.match(/^#\s+(.+)$/m)?.[1]?.trim(); - skills.push({ - kind: 'skill', - skillName: name, - description: String(frontmatter.description || heading || `${name} supporting skill`).trim(), - file: relativePath('skills', entry.name, 'SKILL.md'), - sourcePath: `.toh/skills/${entry.name}/SKILL.md` - }); + return { ...empty, ...manifest }; + } catch { + return empty; } - if (skills.length === 0) throw new Error(`No TOH supporting skills found in ${skillsDir} — cannot generate Codex skills.`); - return skills; } -function wrapperFrontmatter(entry) { - return yaml.dump({ - name: entry.skillName, - description: entry.description.slice(0, 1024), - metadata: { - generator: SKILL_GENERATOR, - version: VERSION, - kind: entry.kind, - source: entry.sourcePath - } - }, { lineWidth: -1, noRefs: true }).trimEnd(); -} - -function renderCommandWrapper(entry) { - const triggers = [entry.command, ...entry.aliases].map((command) => `\`${command}\``).join(', '); - const supporting = entry.skills.length - ? `2. Read every supporting skill first:\n${entry.skills.map((skill) => ` - .toh/skills/${skill}/SKILL.md`).join('\n')}\n3. Execute the workflow in this session, in order.` - : '2. Execute the workflow in this session, in order.'; - return `---\n${wrapperFrontmatter(entry)}\n---\n\n# ${entry.command} - ${entry.description}\n\n> Native Codex CLI skill wrapper for the TOH Framework workflow ${entry.command}.\n> Read the runtime workflow from \`${entry.sourcePath}\`; do not duplicate it here.\n\n## When to use\n\n${entry.description}. Triggers: ${triggers}, or any plain-language request that matches.\n\n## Workflow\n\n1. Read the full workflow definition: \`${entry.sourcePath}\`\n${supporting}\n\n## Codex execution\n\n- Delegate independent plan tasks to the matching \`.codex/agents/.toml\`; keep dependent edits sequential.\n- The parent owns checkpoint verification and updates \`.toh/plan.md\` only after quoting passing output.\n- Codex has no Toh Stop hook; resume from the first unchecked task when a session ends.\n- Agent TOML files own model and reasoning routing; do not reinterpret Claude model names.\n`; -} - -async function readWrapperCatalog(srcDir) { - const commands = await readCommandCatalog(srcDir); - const names = new Set(); - for (const command of commands) { - if (names.has(command.skillName)) throw new Error(`Duplicate Codex skill name: ${command.skillName}`); - names.add(command.skillName); - } - return commands; +async function writeCodexManifest(targetDir, manifest) { + const manifestPath = path.join(targetDir, CODEX_MANIFEST_PATH); + await fs.ensureDir(path.dirname(manifestPath)); + await fs.writeJson(manifestPath, manifest, { spaces: 2 }); } -export async function installCodexSkills(targetDir, srcDir) { - const entries = await readWrapperCatalog(srcDir); - const previous = await readManifest(targetDir); - const nextFiles = Object.fromEntries( - Object.entries(previous.files || {}).filter(([, record]) => record.kind && record.kind !== 'command') - ); - const wanted = new Set(entries.map((entry) => skillFileRelativePath(entry.skillName))); - const root = path.join(targetDir, CODEX_SKILLS_DIR); - await fs.ensureDir(root); - - for (const [relative, record] of Object.entries(previous.files || {})) { - if (!relative.startsWith(`${CODEX_SKILLS_DIR}/`) || wanted.has(relative) || (record.kind && record.kind !== 'command')) continue; - const filePath = path.join(targetDir, relative); - if (await fileMatchesHash(filePath, record.sha256)) { - await fs.remove(filePath); - const parent = path.dirname(filePath); - if ((await fs.readdir(parent)).length === 0) await fs.remove(parent); - } - } - - const installed = []; - for (const entry of entries) { - const relative = skillFileRelativePath(entry.skillName); - const filePath = path.join(targetDir, relative); - const content = renderCommandWrapper(entry); - const existingRecord = previous.files?.[relative]; - const canReplace = !(await fs.pathExists(filePath)) || await fileMatchesHash(filePath, existingRecord?.sha256); - if (!canReplace) continue; - await fs.ensureDir(path.dirname(filePath)); - await fs.writeFile(filePath, content); - nextFiles[relative] = { sha256: sha256(content), kind: entry.kind, source: entry.sourcePath }; - installed.push(entry.skillName); +async function fileMatchesHash(filePath, expectedHash) { + if (!expectedHash || !(await fs.pathExists(filePath))) return false; + try { + return sha256(await fs.readFile(filePath)) === expectedHash; + } catch { + return false; } - await writeManifest(targetDir, { ...previous, files: nextFiles, version: VERSION }); - return installed; } +/** + * Write .codex/agents/.toml for every agent in .toh/agents/. + * Ownership-safe: a file we did not write (or that the user edited since) is + * kept as-is and reported; stale files we wrote for agents that no longer + * exist are removed only when still byte-identical to what we wrote. + */ export async function installCodexAgents(targetDir) { - const agentsDir = path.join(targetDir, '.toh', 'agents'); - if (!(await fs.pathExists(agentsDir))) return []; const agents = await readAgentCatalog(targetDir); - const previous = await readManifest(targetDir); + if (agents.length === 0) return { installed: [], kept: [], total: 0 }; + + const previous = await readCodexManifest(targetDir); const previousAgents = previous.agents || {}; const nextAgents = {}; - const wanted = new Set(agents.map((agent) => agentFileRelativePath(agent.name))); - const root = path.join(targetDir, CODEX_AGENTS_DIR); - await fs.ensureDir(root); - - for (const [relative, record] of Object.entries(previousAgents)) { - if (!relative.startsWith(`${CODEX_AGENTS_DIR}/`) || wanted.has(relative)) continue; - const filePath = path.join(targetDir, relative); - if (await fileMatchesHash(filePath, record.sha256)) { - await fs.remove(filePath); - const parent = path.dirname(filePath); - if ((await fs.readdir(parent)).length === 0) await fs.remove(parent); - } + const wanted = new Set(agents.map((agent) => agentRelPath(agent.name))); + await fs.ensureDir(path.join(targetDir, CODEX_AGENTS_DIR)); + + for (const [rel, record] of Object.entries(previousAgents)) { + if (wanted.has(rel) || !rel.startsWith('.codex/agents/')) continue; + const filePath = path.join(targetDir, rel); + if (await fileMatchesHash(filePath, record.sha256)) await fs.remove(filePath); } const installed = []; + const kept = []; for (const agent of agents) { - const relative = agentFileRelativePath(agent.name); - const filePath = path.join(targetDir, relative); + const rel = agentRelPath(agent.name); + const filePath = path.join(targetDir, rel); const content = translateAgentToCodex(agent); - const existingRecord = previousAgents[relative]; - const canReplace = !(await fs.pathExists(filePath)) || await fileMatchesHash(filePath, existingRecord?.sha256); - if (!canReplace) { - if (existingRecord) nextAgents[relative] = existingRecord; + const record = previousAgents[rel]; + const exists = await fs.pathExists(filePath); + if (exists && !(await fileMatchesHash(filePath, record?.sha256))) { + // Not ours, or edited since we wrote it — the user's file wins. + if (record) nextAgents[rel] = record; + kept.push(agent.name); continue; } - await fs.ensureDir(path.dirname(filePath)); await fs.writeFile(filePath, content); - nextAgents[relative] = { sha256: sha256(content), source: `.toh/agents/${agent.name}.md`, modelIntent: agent.modelIntent }; + nextAgents[rel] = { sha256: sha256(content), source: `.toh/agents/${agent.name}.md`, modelIntent: agent.modelIntent }; installed.push(agent.name); } - await writeManifest(targetDir, { ...previous, agents: nextAgents, version: VERSION }); - return installed; + await writeCodexManifest(targetDir, { ...previous, version: VERSION, agents: nextAgents }); + return { installed, kept, total: agents.length }; } -function agentRoster(srcDir) { - const agentsDir = path.join(srcDir, 'agents'); - return fs.pathExists(agentsDir).then(async (exists) => { - if (!exists) return ''; - const rows = []; - for (const file of (await fs.readdir(agentsDir)).sort()) { - if (!file.endsWith('.md') || file === 'README.md') continue; - const source = path.join(agentsDir, file); - const { frontmatter } = parseFrontmatterDocument(await fs.readFile(source, 'utf8'), source); - const name = frontmatter.name || file.replace(/\.md$/, ''); - const description = String(frontmatter.description || '').replace(/\s+/g, ' ').trim(); - const role = description.match(/^(.*?)\.(?:\s|$)/)?.[1] || description; - rows.push(`| \`${name}\` | ${frontmatter.model || 'sonnet'} | ${role} |`); +/** + * `toh uninstall --ide codex`: remove ONLY the native agent files this + * installer wrote (hash-verified) plus the manifest. AGENTS.md and + * .codex/config.toml are shared surfaces (ZCode reads AGENTS.md too) and are + * handled by the full uninstall planner in installer/uninstall.js. + */ +export async function uninstallCodex(targetDir, options = {}) { + const { dryRun = false, backup = true } = options; + const manifest = await readCodexManifest(targetDir); + const removed = []; + const kept = []; + for (const [rel, record] of Object.entries(manifest.agents || {})) { + if (!rel.startsWith('.codex/agents/') || !rel.endsWith('.toml')) continue; + const filePath = path.join(targetDir, rel); + if (!(await fs.pathExists(filePath))) continue; + if (await fileMatchesHash(filePath, record.sha256)) removed.push({ rel, filePath }); + else kept.push(rel); + } + const result = { + removedAgents: removed.map((item) => path.basename(item.rel, '.toml')).sort(), + keptAgents: kept.map((rel) => path.basename(rel, '.toml')).sort(), + dryRun, + backupPath: null + }; + if (dryRun) return result; + + if (backup && removed.length > 0) { + const backupDir = path.join(targetDir, '.toh-uninstall-backup', `codex-agents-${Date.now()}`); + for (const item of removed) { + const destination = path.join(backupDir, item.rel); + await fs.ensureDir(path.dirname(destination)); + await fs.copy(item.filePath, destination); } - return ['| Agent | Model | Role |', '|-------|-------|------|', ...rows].join('\n'); - }); + result.backupPath = backupDir; + } + for (const item of removed) await fs.remove(item.filePath); + const agentsDir = path.join(targetDir, CODEX_AGENTS_DIR); + if (await fs.pathExists(agentsDir) && (await fs.readdir(agentsDir)).length === 0) await fs.remove(agentsDir); + const manifestPath = path.join(targetDir, CODEX_MANIFEST_PATH); + if (await fs.pathExists(manifestPath)) await fs.remove(manifestPath); + return result; } -function renderSkillTable(entries) { - return [ - '| Skill | Use it to |', - '|-------|-----------|', - ...entries.map((entry) => `| \`$${entry.skillName}\` | ${entry.description} |`) - ].join('\n'); -} +/** + * Create memory template files for the Memory System (v1.7.0) + * Now includes architecture.md and components.md for Code Architecture Tracking + */ +async function createMemoryFiles(memoryDir, language = 'en') { + const timestamp = new Date().toISOString().split('T')[0]; -function runtimeIdentity(ide, probedSubagents) { - const name = ide === 'zcode' ? 'ZCode' : 'Codex CLI'; - const capabilities = probedSubagents - ? 'Native subagents are available per the installed Codex feature probe; delegate independent work and keep dependent work sequential.' - : ide === 'zcode' - ? 'Native subagents are not verified for this runtime; execute the TOH LOOP sequentially.' - : 'Native Codex CLI subagents and workflows are available; delegate independent work and keep dependent work sequential.'; - return `Runtime Identity: you are running in ${name}. ${capabilities} Implement -> run the story checkpoint -> quote actual output -> fix if red (max 5 tries, 3 consecutive failures = mark BLOCKED and move on) -> tick the checkbox -> next story without asking. Interrupted runs resume from the first unchecked task in .toh/plan.md unless the plan has terminal status. Close every stage with Status/Result/Evidence/exactly 3 next actions.`; -} + const activeContent = language === 'th' + ? `# 🔥 Active Task\n\n## Current Focus\n[รอคำสั่งจากผู้ใช้]\n\n## In Progress\n- (ยังไม่มี)\n\n## Next Steps\n- รอคำสั่งจากผู้ใช้\n\n---\n*Last updated: ${timestamp}*\n` + : `# 🔥 Active Task\n\n## Current Focus\n[Waiting for user command]\n\n## In Progress\n- (none)\n\n## Next Steps\n- Waiting for user command\n\n---\n*Last updated: ${timestamp}*\n`; -function generateAgentsBlock(entries, language, ide, roster, probedSubagents) { - const thai = language === 'th'; - const isCodex = ide === 'codex'; - const title = thai - ? `คุณคือ **Toh Framework Agent** ที่รันอยู่บน ${isCodex ? 'Codex CLI' : 'ZCode'} - ช่วย Solo Developer สร้าง SaaS จนจบ` - : `You are the **Toh Framework Agent** running in ${isCodex ? 'Codex CLI' : 'ZCode'}, helping solo developers build SaaS systems by themselves.`; - const state = thai - ? '- `.toh/plan.md` + `.toh/progress.md` - แผนคือไฟล์และใช้ resume จาก task แรกที่ยังไม่ติ๊ก\n- `.toh/memory/` - memory 7 ไฟล์' - : '- `.toh/plan.md` + `.toh/progress.md` - the plan is a file; resume from the first unchecked task\n- `.toh/memory/` - 7-file memory'; - const commandNote = isCodex - ? '- Invoke a workflow with `$` or browse with `/skills`.\n- Native project agents live in `.codex/agents/*.toml` and define model, reasoning, and sandbox.\n- If the user types `/toh-*`, interpret it as a backward-compatible plain-text request, not a native slash command.' - : '- The same 14 workflows are available from `.agents/commands/` and `.agents/skills/`.\n- ZCode subagent support is deliberately not claimed until it is verified live.'; - return `${AGENTS_BLOCK_START} -# Toh Framework + const summaryContent = language === 'th' + ? `# 📋 Project Summary\n\n## Project Overview\n- Name: [ชื่อโปรเจค]\n- Tech Stack: Next.js 16, Tailwind, shadcn/ui, Zustand, Supabase\n\n## Completed Features\n- (ยังไม่มี)\n\n## Important Notes\n- ใช้ Toh Framework v${VERSION}\n\n---\n*Last updated: ${timestamp}*\n` + : `# 📋 Project Summary\n\n## Project Overview\n- Name: [Project Name]\n- Tech Stack: Next.js 16, Tailwind, shadcn/ui, Zustand, Supabase\n\n## Completed Features\n- (none)\n\n## Important Notes\n- Using Toh Framework v${VERSION}\n\n---\n*Last updated: ${timestamp}*\n`; -> **"Type Once, Have it all!"** - AI-Orchestration Driven Development + const decisionsContent = language === 'th' + ? `# 🧠 Key Decisions\n\n## Architecture Decisions\n| Date | Decision | Reason |\n|------|----------|--------|\n| ${timestamp} | ใช้ Toh Framework | AI-Orchestration Driven Development |\n\n---\n*Last updated: ${timestamp}*\n` + : `# 🧠 Key Decisions\n\n## Architecture Decisions\n| Date | Decision | Reason |\n|------|----------|--------|\n| ${timestamp} | Use Toh Framework | AI-Orchestration Driven Development |\n\n---\n*Last updated: ${timestamp}*\n`; -## Project Memory + // architecture.md (v1.7.0 - Code Architecture Tracking) + const architectureContent = `# 🏗️ Project Architecture -This file serves as project memory for ${isCodex ? 'Codex CLI' : 'ZCode'}. It contains the Toh Framework configuration and agent definitions. +> Semantic overview of project structure for AI context loading +> **Update:** After any structural changes (new pages, routes, modules, services) -This file is a compact index. Full specs live on disk and MUST be read at runtime: -- Commands -> \`.toh/commands/toh-.md\` -- Agents -> \`.toh/agents/.md\` -- Skills -> \`.toh/skills//SKILL.md\` +--- -## Identity +## 📁 Entry Points -${title} +| Type | Path | Purpose | +|------|------|---------| +| Main | \`app/page.tsx\` | Landing/Home page | +| Layout | \`app/layout.tsx\` | Root layout with providers | +| API | \`app/api/\` | API routes (if any) | -${renderCapabilitiesSection(ide)} +--- -${runtimeIdentity(ide, probedSubagents)} +## 🗂️ Core Modules -## Using TOH +### \`/app\` - Pages & Routes -${isCodex ? 'TOH provides native workflow skills and native custom agents:' : 'TOH uses shared open project surfaces:'} +| Route | File | Description | Key Functions | +|-------|------|-------------|---------------| +| \`/\` | \`app/page.tsx\` | Landing page | - | -${renderSkillTable(entries)} +### \`/components\` - UI Components -${commandNote} +| Folder | Purpose | Key Files | +|--------|---------|-----------| +| \`ui/\` | shadcn/ui components | button, card, input, etc. | +| \`layout/\` | Layout components | Navbar, Sidebar, Footer | +| \`features/\` | Feature-specific | Per feature components | -## TOH runtime and state +### \`/lib\` - Utilities & Services -${state} -- \`.toh/skills/\` - \`.toh/commands/\` - \`.toh/capabilities.json\` +| File | Purpose | Key Functions | +|------|---------|---------------| +| \`lib/utils.ts\` | Utility functions | cn(), formatDate() | -${isCodex && roster ? `## Native project agents\n\n${roster}\n` : ''} -## TOH execution contract +--- -- Start with the first unchecked task in \`.toh/plan.md\`. -- Delegate only genuinely independent work on disjoint files. -- The parent re-runs each checkpoint before ticking a task. -- Results include Status, Result, Evidence, Files, and Blockers. +## 🔄 Data Flow Pattern -${AGENTS_BLOCK_END}`; -} +User Action → Component → Zustand Store → API/Lib → Database (Supabase) -export function assertAgentsMdSize(content) { - const bytes = Buffer.byteLength(content, 'utf8'); - if (bytes > AGENTS_MAX_BYTES) throw new Error(`AGENTS.md is ${bytes} bytes; Codex limit is ${AGENTS_MAX_BYTES} bytes.`); -} +--- -async function buildAgentsBlock(srcDir, language, ide, options) { - const entries = await readWrapperCatalog(srcDir); - const roster = await agentRoster(srcDir); - let probedSubagents = false; - if (ide === 'codex' && options.allowProbedSubagents === true) { - const probe = await probeCodexCapabilitiesCached(); - probedSubagents = probe.ok === true && probe.overrides?.subagents === 'native'; - } - return transformCommand(generateAgentsBlock(entries, language, ide, roster, probedSubagents), ide); -} +## 🔌 External Services -export async function writeAgentsMd(targetDir, srcDir, language = 'en', ide = 'codex', options = {}) { - const block = await buildAgentsBlock(srcDir, language, ide, options); - if (Buffer.byteLength(block, 'utf8') > AGENTS_MAX_BYTES) { - const error = new Error(`Generated AGENTS.md TOH block exceeds the ${AGENTS_MAX_BYTES}-byte Codex limit.`); - error.fatal = true; - throw error; - } - const agentsPath = path.join(targetDir, 'AGENTS.md'); - const existing = await fs.pathExists(agentsPath) ? await fs.readFile(agentsPath, 'utf8') : ''; - const stripped = existing.replace(AGENTS_BLOCK_RE, '').trimEnd(); - const next = stripped ? `${stripped}\n\n${block}\n` : `${block}\n`; - assertAgentsMdSize(next); - await fs.writeFile(agentsPath, next); - await updateManifest(targetDir, { agentsBlockSha256: sha256(block) }); - return Buffer.byteLength(block, 'utf8'); +| Service | Purpose | Config Location | +|---------|---------|-----------------| +| Supabase | Backend (Auth, DB) | \`lib/supabase/\` | + +--- + +## 📝 Notes + +- Using Toh Framework v${VERSION} +- Architecture tracking enabled + +--- +*Last updated: ${timestamp}* +`; + + // components.md (v1.7.0 - Component Registry) + const componentsContent = `# 📦 Component Registry + +> Quick reference for all project components, hooks, and utilities +> **Update:** After creating/modifying any component, hook, or utility + +--- + +## 📄 Pages + +| Route | File | Description | Key Dependencies | +|-------|------|-------------|------------------| +| \`/\` | \`app/page.tsx\` | Landing page | - | + +--- + +## 🧩 Components + +### Layout Components + +| Component | Location | Key Props | Used By | +|-----------|----------|-----------|---------| +| (none yet) | - | - | - | + +### Feature Components + +| Component | Location | Key Props | Used By | +|-----------|----------|-----------|---------| +| (none yet) | - | - | - | + +--- + +## 🪝 Custom Hooks + +| Hook | Location | Purpose | Returns | +|------|----------|---------|---------| +| (none yet) | - | - | - | + +--- + +## 🏪 Zustand Stores + +| Store | Location | State Shape | Key Actions | +|-------|----------|-------------|-------------| +| (none yet) | - | - | - | + +--- + +## 🛠️ Utility Functions + +| Function | Location | Purpose | Params | +|----------|----------|---------|--------| +| cn | \`lib/utils.ts\` | Merge Tailwind classes | \`...inputs\` | + +--- + +## 📊 Component Statistics + +| Category | Count | +|----------|-------| +| Pages | 1 | +| Components | 0 | +| Hooks | 0 | +| Stores | 0 | + +--- +*Last updated: ${timestamp}* +`; + + // changelog.md (v1.8.0 - Session Changelog) + const changelogContent = `# 📝 Session Changelog + +## [Current Session] - ${timestamp} + +### Changes Made +| Agent | Action | File/Component | +|-------|--------|----------------| +| - | - | - | + +### Next Session TODO +- [ ] Continue from: [last task] + +--- +*Auto-updated by agents after each task* +`; + + // agents-log.md (v1.8.0 - Agent Activity Log) + const agentsLogContent = `# 🤖 Agents Activity Log + +## Recent Activity +| Time | Agent | Task | Status | Files | +|------|-------|------|--------|-------| +| - | - | - | - | - | + +## Agent Statistics +- Total Tasks: 0 +- Success Rate: 100% + +--- +*Auto-updated by agents during execution* +`; + + // Seed the 7 memory files - ONLY where absent (issue #2: a reinstall + // must never clobber live memory; even an empty file is the user's). + await seedFileIfAbsent(path.join(memoryDir, 'active.md'), activeContent); + await seedFileIfAbsent(path.join(memoryDir, 'summary.md'), summaryContent); + await seedFileIfAbsent(path.join(memoryDir, 'decisions.md'), decisionsContent); + await seedFileIfAbsent(path.join(memoryDir, 'architecture.md'), architectureContent); + await seedFileIfAbsent(path.join(memoryDir, 'components.md'), componentsContent); + await seedFileIfAbsent(path.join(memoryDir, 'changelog.md'), changelogContent); + await seedFileIfAbsent(path.join(memoryDir, 'agents-log.md'), agentsLogContent); } -export async function updateAgentsMd(targetDir, entries, language = 'en', ide = 'codex', options = {}) { +/** + * Build and write the root AGENTS.md marker block. + * + * Shared surface: Codex reads AGENTS.md as project memory, and so does ZCode + * (Z.ai) — same filename, same marker block, three runtime sentences apart. + * Exported so zcode.js reuses this instead of forking a second generator. + * + * Content outside is always preserved. + * Returns the byte size of the generated block. + */ +export async function writeAgentsMd(targetDir, srcDir, language = 'en', ide = 'codex', options = {}) { + // v2.1.x (issue #2 problem 2): the Runtime Identity sentence may claim + // probed native subagents ONLY when the caller says codex is the SOLE + // reader/writer of AGENTS.md (options.allowProbedSubagents — install.js + // sets it to false whenever ZCode is selected now or was declared earlier) + // AND the live probe of the installed codex CLI verified the feature. + // Probe failure, ZCode co-install, or zcode-only installs all keep the + // conservative sentence byte-identical. let probedSubagents = false; if (ide === 'codex' && options.allowProbedSubagents === true) { const probe = await probeCodexCapabilitiesCached(); probedSubagents = probe.ok === true && probe.overrides?.subagents === 'native'; } - const block = transformCommand(generateAgentsBlock(entries, language, ide, '', probedSubagents), ide); - const agentsPath = path.join(targetDir, 'AGENTS.md'); - const existing = await fs.pathExists(agentsPath) ? await fs.readFile(agentsPath, 'utf8') : ''; - const stripped = existing.replace(AGENTS_BLOCK_RE, '').trimEnd(); - const next = stripped ? `${stripped}\n\n${block}\n` : `${block}\n`; - assertAgentsMdSize(next); - await fs.writeFile(agentsPath, next); - await updateManifest(targetDir, { agentsBlockSha256: sha256(block) }); - return agentsPath; -} + // Read all agents — v2.1 (W1): embed a compact roster table ONLY. + // Full agent bodies used to be inlined here, which pushed AGENTS.md to + // ~117 KB while Codex silently truncates project docs at 32 KiB combined — + // 6/8 agents and everything after them were dropped without warning. + // Full specs live in .toh/agents/ and are read at runtime instead (the same + // .toh/ runtime-read pattern cursor.js uses). + const srcAgentsDir = path.join(srcDir, 'agents'); + let agentRoster = ''; + + if (await fs.pathExists(srcAgentsDir)) { + const agentFiles = (await fs.readdir(srcAgentsDir)).sort(); + const rows = []; + for (const file of agentFiles) { + if (file.endsWith('.md') && file !== 'README.md') { + const raw = await fs.readFile(path.join(srcAgentsDir, file), 'utf-8'); + const agentName = file.replace('.md', ''); + const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/); + if (!fmMatch) { + throw new Error(`[toh-framework] src/agents/${file} has no YAML frontmatter — cannot build the Codex agent roster.`); + } + // Agents carry superset frontmatter (name/description/tools/model/ + // skills/triggers/...). The roster needs name, model and description + // only; a parse failure here is a packaging bug — let it throw. + const fm = yaml.load(fmMatch[1]) || {}; + const desc = String(fm.description || '').replace(/\s+/g, ' ').trim(); + // Role = first sentence of the description ('.md' never terminates — + // boundary is a period followed by whitespace/end). + const roleMatch = desc.match(/^(.*?)\.(?:\s|$)/); + const role = roleMatch ? roleMatch[1].trim() : desc; + // 'Delegate when' sentence (colon optional — root-cause-debugger + // phrases it without one), same boundary rule. + const delegateMatch = desc.match(/Delegate when:?\s*([\s\S]*?)(?:\.(?:\s|$)|$)/); + const delegateWhen = delegateMatch ? delegateMatch[1].trim() : '(see agent file)'; + rows.push(`| \`${fm.name || agentName}\` | ${fm.model || 'sonnet'} | ${role} | ${delegateWhen} |`); + } + } + agentRoster = [ + '| Agent | Model | Role | Delegate when |', + '|-------|-------|------|---------------|', + ...rows + ].join('\n'); + } -function configBlock(addFeatures, addQuota) { - const lines = [CONFIG_BLOCK_START, '# Generated by Toh Framework for Codex CLI.']; - if (addQuota) lines.push(`project_doc_max_bytes = ${CODEX_PROJECT_DOC_MAX_BYTES}`); - if (addFeatures) lines.push('[features]', 'multi_agent = true'); - lines.push(CONFIG_BLOCK_END); - return `${lines.join('\n')}\n`; -} + // v2.0: run the assembled markdown through the shared marker transform so any + // blocks in embedded command/agent markdown are removed and + // blocks are unwrapped for Codex (idempotent, additive). + const agentsMd = transformCommand( + language === 'th' + ? generateAgentsMdTH(agentRoster, ide, probedSubagents) + : generateAgentsMdEN(agentRoster, ide, probedSubagents), + ide + ); -function insertConfig(content, block, needsRootQuota) { - if (!content) return block; - if (!needsRootQuota) return `${content}\n${block}`; - const firstTable = content.search(/^\s*\[/m); - if (firstTable < 0) return `${content}\n${block}`; - return `${content.slice(0, firstTable).trimEnd()}\n\n${block}${content.slice(firstTable)}`; -} + // W1 hard size assertion: never ship a block Codex would silently truncate. + const tohBlockBytes = Buffer.byteLength(agentsMd, 'utf-8'); + if (tohBlockBytes > MAX_TOH_BLOCK_BYTES) { + const err = new Error( + `[toh-framework] Generated AGENTS.md TOH block is ${tohBlockBytes} bytes — over the ` + + `${MAX_TOH_BLOCK_BYTES}-byte hard budget (Codex truncates project docs at 32 KiB combined ` + + `with any user content). Refusing to install a silently-truncated AGENTS.md; slim the ` + + `generator in installer/ide-handlers/codex.js.` + ); + // Hard size budget: over-budget output would be silently truncated by + // Codex. install.js aborts the whole install (non-zero exit) on fatal errors. + err.fatal = true; + throw err; + } -export async function setupCodexConfig(targetDir) { - const configPath = path.join(targetDir, '.codex', 'config.toml'); - const existing = await fs.pathExists(configPath) ? await fs.readFile(configPath, 'utf8') : ''; - const withoutToh = existing.replace(CONFIG_BLOCK_RE, '').trimEnd(); - let hasQuota = false; - let hasFeatures = false; - if (withoutToh) { - try { - const parsed = parseToml(withoutToh); - hasQuota = Object.hasOwn(parsed, 'project_doc_max_bytes'); - hasFeatures = Boolean(parsed.features && typeof parsed.features === 'object'); - } catch { - const root = withoutToh.split(/^\s*\[/m, 1)[0]; - hasQuota = /^\s*project_doc_max_bytes\s*=\s*/m.test(root); - hasFeatures = /^\s*\[features\]\s*$/m.test(withoutToh); + // Check if AGENTS.md exists + const agentsPath = path.join(targetDir, 'AGENTS.md'); + + if (await fs.pathExists(agentsPath)) { + // Read existing content + let existing = await fs.readFile(agentsPath, 'utf-8'); + + // Replace TOH section if exists, otherwise append + if (existing.includes('')) { + existing = existing.replace( + /[\s\S]*/, + agentsMd.trim() + ); + await fs.writeFile(agentsPath, existing); + } else { + await fs.appendFile(agentsPath, '\n\n' + agentsMd); } + } else { + await fs.writeFile(agentsPath, agentsMd); } - if (hasQuota && hasFeatures) { - await updateManifest(targetDir, { configSha256: null }); - return configPath; - } - const block = configBlock(!hasFeatures, !hasQuota); - const next = insertConfig(withoutToh, block, !hasQuota); - await fs.ensureDir(path.dirname(configPath)); - await fs.writeFile(configPath, next); - await updateManifest(targetDir, { configSha256: sha256(next) }); - return configPath; + + return tohBlockBytes; } -export async function ensureTohRuntime(targetDir) { +export async function setupCodex(targetDir, srcDir, language = 'en', options = {}) { + // Create .toh/memory directory structure (v1.1.0 - Memory System) const memoryDir = path.join(targetDir, '.toh', 'memory'); await fs.ensureDir(path.join(memoryDir, 'archive')); - const today = new Date().toISOString().split('T')[0]; - const seeds = { - 'active.md': '# Active Task\n\n[No active task - Waiting for user command]\n', - 'summary.md': '# Project Summary\n\n[No project summary yet]\n', - 'decisions.md': `# Key Decisions\n\n| Date | Decision | Reason |\n|------|----------|--------|\n| ${today} | Use Toh Framework v${VERSION} | AI-Orchestration Driven Development |\n`, - 'changelog.md': `# Session Changelog\n\n## Current Session - ${today}\n`, - 'agents-log.md': '# Agents Activity Log\n\n', - 'architecture.md': '# Code Architecture\n\n', - 'components.md': '# Component Registry\n\n' - }; - for (const [file, content] of Object.entries(seeds)) { - await seedFileIfAbsent(path.join(memoryDir, file), content); - } - await seedFileIfAbsent(path.join(targetDir, '.toh', 'plan.md'), `# Plan: (no active plan yet)\nStatus: draft\nCreated: ${today} by toh-framework installer\n`); - await seedFileIfAbsent(path.join(targetDir, '.toh', 'progress.md'), '# Progress Ledger\n\n'); -} + await createMemoryFiles(memoryDir, language); -export async function setupCodex(targetDir, srcDir, language = 'en', options = {}) { - await ensureTohRuntime(targetDir); - const entries = await readWrapperCatalog(srcDir); - await setupCodexConfig(targetDir); - const installed = await installCodexSkills(targetDir, srcDir); - const installedAgents = await installCodexAgents(targetDir); await writeAgentsMd(targetDir, srcDir, language, 'codex', options); - return `${CODEX_SKILLS_DIR}/ (${installed.length}/${entries.length} workflows) + ${CODEX_AGENTS_DIR}/ (${installedAgents.length} agents) + AGENTS.md + .codex/config.toml`; -} -async function backupFiles(targetDir, files) { - if (files.length === 0) return null; - const backupDir = path.join(targetDir, '.toh-backups', `codex-${Date.now()}`); - for (const relative of files) { - const source = path.join(targetDir, relative); - if (!(await fs.pathExists(source))) continue; - const destination = path.join(backupDir, relative); - await fs.ensureDir(path.dirname(destination)); - await fs.copy(source, destination); + // W1 belt-and-braces: project-scoped .codex/config.toml raising Codex's + // project-doc budget (officially supported key, per config-reference), so + // even a large pre-existing user AGENTS.md above our marker cannot push the + // combined file past the read cutoff. NEVER overwrite a user's config.toml. + const codexConfigPath = path.join(targetDir, '.codex', 'config.toml'); + if (!(await fs.pathExists(codexConfigPath))) { + await fs.ensureDir(path.dirname(codexConfigPath)); + await fs.writeFile( + codexConfigPath, + `# Generated by Toh Framework v${VERSION}\n` + + `# Raises Codex's per-project doc read budget (default 32768 bytes) so the\n` + + `# full AGENTS.md — including the Toh Framework block — is always loaded.\n` + + `# Safe to edit; the installer never overwrites an existing config.toml.\n` + + `project_doc_max_bytes = 131072\n` + ); } - return backupDir; + + // v2.2: native Codex agents, one TOML per Toh agent (ownership-safe). + const agents = await installCodexAgents(targetDir); + const keptNote = agents.kept.length ? `, ${agents.kept.length} kept as edited` : ''; + return `AGENTS.md + .codex/agents/ (${agents.installed.length}/${agents.total} agents${keptNote})`; } -export async function uninstallCodex(targetDir, options = {}) { - const { dryRun = false, backup = true } = options; - const manifest = await readManifest(targetDir); - const skillRemovals = []; - const agentRemovals = []; - const backupPaths = []; - - for (const [relative, record] of Object.entries(manifest.files || {})) { - if (!relative.startsWith(`${CODEX_SKILLS_DIR}/`) || !relative.endsWith('/SKILL.md')) continue; - const filePath = path.join(targetDir, relative); - if (await fileMatchesHash(filePath, record.sha256)) { - skillRemovals.push({ filePath, name: path.basename(path.dirname(filePath)) }); - backupPaths.push(relative); - } - } - for (const [relative, record] of Object.entries(manifest.agents || {})) { - if (!relative.startsWith(`${CODEX_AGENTS_DIR}/`) || !relative.endsWith('.toml')) continue; - const filePath = path.join(targetDir, relative); - if (await fileMatchesHash(filePath, record.sha256)) { - agentRemovals.push({ filePath, name: path.basename(filePath, '.toml') }); - backupPaths.push(relative); - } - } +function generateAgentsMdEN(agentRoster, ide = 'codex', probedSubagents = false) { + const rt = AGENTS_MD_RUNTIMES[ide] || AGENTS_MD_RUNTIMES.codex; + return ` +# 🎯 Toh Framework - let agentsMd = 'absent'; - const agentsPath = path.join(targetDir, 'AGENTS.md'); - if (manifest.agentsBlockSha256 && await fs.pathExists(agentsPath)) { - const existing = await fs.readFile(agentsPath, 'utf8'); - const match = existing.match(/[\s\S]*?/); - if (match && sha256(match[0]) === manifest.agentsBlockSha256) { - agentsMd = existing.replace(AGENTS_BLOCK_RE, '').trim() ? 'updated' : 'removed'; - backupPaths.push('AGENTS.md'); - } - } +> **"Type Once, Have it all!"** - AI-Orchestration Driven Development - let config = 'absent'; - const configPath = path.join(targetDir, '.codex', 'config.toml'); - if (manifest.configSha256 && await fileMatchesHash(configPath, manifest.configSha256)) { - const existing = await fs.readFile(configPath, 'utf8'); - config = existing.replace(CONFIG_BLOCK_RE, '').trim() ? 'updated' : 'removed'; - backupPaths.push(path.relative(targetDir, configPath)); - } +## Project Memory - const result = { - removedSkills: skillRemovals.map((item) => item.name).sort(), - removedAgents: agentRemovals.map((item) => item.name).sort(), - agentsMd, - config, - dryRun, - backupPath: null - }; - if (dryRun) return result; - if (backup) result.backupPath = await backupFiles(targetDir, [...new Set(backupPaths)]); +${rt.memoryEN} - for (const item of [...skillRemovals, ...agentRemovals]) { - await fs.remove(item.filePath); - const parent = path.dirname(item.filePath); - if ((await fs.readdir(parent)).length === 0) await fs.remove(parent); - } - if (agentsMd !== 'absent') { - const stripped = (await fs.readFile(agentsPath, 'utf8')).replace(AGENTS_BLOCK_RE, '').trim(); - if (stripped) await fs.writeFile(agentsPath, `${stripped}\n`); - else await fs.remove(agentsPath); - } - if (config !== 'absent') { - const stripped = (await fs.readFile(configPath, 'utf8')).replace(CONFIG_BLOCK_RE, '').trim(); - if (stripped) await fs.writeFile(configPath, `${stripped}\n`); - else await fs.remove(configPath); - } - const manifestPath = path.join(targetDir, MANIFEST_PATH); - if (await fs.pathExists(manifestPath)) await fs.remove(manifestPath); - return result; +This file is a compact index. Full specs live on disk and MUST be read at runtime: +- Commands → \`.toh/commands/toh-.md\` +- Agents → \`.toh/agents/.md\` +- Skills → \`.toh/skills//SKILL.md\` + +## Identity + +You are the **Toh Framework Agent** - an AI that helps Solo Developers build SaaS systems by themselves. + +${renderCapabilitiesSection(ide)} + +${runtimeIdentityLine(rt, probedSubagents)} + +## Core Philosophy (AODD - AI-Orchestration Driven Development) + +1. **Natural Language → Tasks** - Users give commands in plain language, you break them into tasks +2. **Orchestrator → Agents** - Automatically invoke relevant agents to complete work +3. **Users Don't Touch the Process** - No questions, no waiting, just deliver results +4. **Test → Fix → Loop** - Test, fix issues, repeat until passing + +## Tech Stack (Fixed - NEVER CHANGE) + +| Category | Technology | +|----------|------------| +| Framework | Next.js 16 (App Router) | +| Styling | Tailwind CSS + shadcn/ui | +| State | Zustand | +| Forms | React Hook Form + Zod | +| Backend | Supabase | +| Testing | Playwright | +| Language | TypeScript (strict) | + +## Language Rules + +- **Response Language:** Respond in the same language the user uses (if unclear, default to English) +- **UI Labels/Buttons:** English (Save, Cancel, Dashboard) +- **Mock Data:** English names, addresses, phone numbers +- **Code Comments:** English +- **Validation Messages:** English + +If user writes in Thai, respond in Thai. + +## 🚨 Command Recognition (CRITICAL) + +> **YOU MUST recognize and execute these commands immediately!** +> When user types ANY of these patterns, treat them as direct commands. +> The table below is an INDEX only — when a command is invoked, read +> \`.toh/commands/toh-.md\` (e.g. \`.toh/commands/toh-vibe.md\`) and follow it. +> That file is the command's full behavior spec. + +| Command | Shortcuts (ALL VALID) | Purpose | +|---------|----------------------|---------| +| \`/toh-help\` | \`/toh-h\`, \`toh help\`, \`toh h\` | Show all commands | +| \`/toh-plan\` | \`/toh-p\`, \`toh plan\`, \`toh p\` | **THE BRAIN** — writes .toh/plan.md, one approval, then builds autonomously | +| \`/toh-vibe\` | \`/toh-v\`, \`toh vibe\`, \`toh v\` | Create new project with UI + Logic + Mock Data | +| \`/toh-ui\` | \`/toh-u\`, \`toh ui\`, \`toh u\` | Create UI - Pages, Components, Layouts | +| \`/toh-dev\` | \`/toh-d\`, \`toh dev\`, \`toh d\` | Add Logic - TypeScript, Zustand, Forms | +| \`/toh-design\` | \`/toh-ds\`, \`toh design\`, \`toh ds\` | Improve Design - Make it look professional | +| \`/toh-test\` | \`/toh-t\`, \`toh test\`, \`toh t\` | Test system - Auto test & fix until passing | +| \`/toh-connect\` | \`/toh-c\`, \`toh connect\`, \`toh c\` | Connect Backend - Supabase, Auth, RLS | +| \`/toh-line\` | \`/toh-l\`, \`toh line\`, \`toh l\` | LINE MINI App - convert (LIFF SDK) | +| \`/toh-mobile\` | \`/toh-m\`, \`toh mobile\`, \`toh m\` | Mobile App - PWA / Capacitor | +| \`/toh-fix\` | \`/toh-f\`, \`toh fix\`, \`toh f\` | Fix bugs - Debug and fix issues | +| \`/toh-ship\` | \`/toh-s\`, \`toh ship\`, \`toh s\` | Deploy - Vercel, Production ready | +| \`/toh-protect\` | \`/toh-pt\`, \`toh protect\`, \`toh pt\` | Security audit - Full security check | + +### ⚡ Execution Rules: + +1. **Instant Recognition** - When you see \`/toh-\` or \`toh \` prefix, this is a COMMAND +2. **Read the command file first** - \`.toh/commands/toh-.md\` is the full spec; never execute from this index alone +3. **Check for Description** - Does the command have a description after it? + - ✅ **Has description** → Execute immediately, no confirmation + - ❓ **No description** → Introduce yourself as that command's agent and ask what to do (e.g. "I'm the **Vibe Agent** 🎨. What system would you like me to build?"). Exception: \`/toh-help\` always runs immediately +4. **Follow Memory Protocol** - Read/write \`.toh/memory/\` before/after + +## Memory System (Auto, 7 files — Tiered Loading) + +Toh Framework has automatic memory at \`.toh/memory/\`. Read only what the task needs: +- **Tier 1 (ALWAYS read, ~800 tokens):** \`active.md\` (current task) + \`summary.md\` (project overview) +- **Tier 2 (per task type):** \`architecture.md\` + \`components.md\` for build/code work; \`changelog.md\` for debug work +- **Tier 3 (only when referenced):** \`decisions.md\` (past decisions) + \`agents-log.md\` (agent activity) +- \`archive/\` - Historical data (on-demand only) + +## 🚨 MANDATORY: Memory Protocol (Tiered Loading) + +> **CRITICAL:** You MUST follow this protocol EVERY time! Never read all 7 files by reflex. + +### BEFORE Starting ANY Work: +1. Check \`.toh/memory/\` folder exists +2. Read Tier 1: \`.toh/memory/active.md\` + \`.toh/memory/summary.md\` +3. Read Tier 2 for this task type (build/code → \`architecture.md\` + \`components.md\`; debug → \`changelog.md\`) +4. Read Tier 3 (\`decisions.md\`, \`agents-log.md\`) ONLY when referenced +5. If files empty but project has code → ANALYZE and populate first! +6. Acknowledge: "Memory loaded! [Brief context]" + +### AFTER Completing ANY Work (write per relevance): +1. Update \`.toh/memory/active.md\` - ALWAYS (what was done, next steps) +2. Update \`.toh/memory/summary.md\` - when the project shape changes (feature done / new structure) +3. Update \`.toh/memory/architecture.md\` / \`components.md\` - when modules/stores/hooks/utils change +4. Update \`.toh/memory/changelog.md\` + \`agents-log.md\` - record the change and which agent did it +5. Update \`.toh/memory/decisions.md\` - if a real decision was made +6. Confirm: "Memory saved ✅" + +### ⚠️ CRITICAL RULES: +- NEVER start work without reading Tier 1 (active.md + summary.md)! +- NEVER finish work without updating active.md! +- Read Tier 2 / Tier 3 only when the task type or a reference calls for it! +- Memory files must ALWAYS be in English! + +## Behavior Rules + +1. **Don't ask basic questions** - Make decisions yourself +2. **Use the fixed tech stack** - Never change it +3. **Respond in English** - All communication in English +4. **English Mock Data** - Use English names, addresses, phone numbers +5. **UI First** - Create working UI before backend +6. **Production Ready** - Not a prototype + +## Mock Data Examples + +Use realistic English data: +- Names: John, Mary, Michael, Sarah +- Last names: Smith, Johnson, Williams +- Cities: New York, Los Angeles, Chicago +- Phone: (555) 123-4567 +- Email: john.smith@example.com + +## Agents (roster — full specs in \`.toh/agents/\`) + +${agentRoster} + +This table is a summary ONLY. Before acting as (or delegating to) any agent, +read \`.toh/agents/.md\` — the agent's workflow, rules, tool limits, and +quality bar live in that file, not here. + +## 🚨 MANDATORY: Skills & Agents Loading + +> **CRITICAL:** Before executing ANY /toh- command, you MUST load the required skills! + +### Command → Skills Map + +| Command | Load These Skills (from \`.toh/skills/\`) | +|---------|------------------------------------------| +| \`/toh\` | \`smart-routing\`, \`orchestration-protocol\`, \`engineer-harness\` | +| \`/toh-vibe\` | \`vibe-orchestrator\`, \`orchestration-protocol\`, \`premium-experience\`, \`design-craft\`, \`ui-first-builder\`, \`engineer-harness\` | +| \`/toh-ui\` | \`ui-first-builder\`, \`design-craft\`, \`engineer-harness\` | +| \`/toh-dev\` | \`dev-engineer\`, \`backend-engineer\`, \`engineer-harness\` | +| \`/toh-design\` | \`design-craft\`, \`premium-experience\` | +| \`/toh-test\` | \`test-engineer\`, \`debug-protocol\`, \`error-handling\` | +| \`/toh-connect\` | \`backend-engineer\`, \`integrations\` | +| \`/toh-plan\` | \`plan-orchestrator\`, \`orchestration-protocol\`, \`business-context\`, \`smart-routing\`, \`engineer-harness\` | +| \`/toh-fix\` | \`debug-protocol\`, \`error-handling\`, \`test-engineer\` | +| \`/toh-line\` | \`platform-specialist\`, \`integrations\` | +| \`/toh-mobile\` | \`platform-specialist\`, \`ui-first-builder\` | +| \`/toh-ship\` | \`version-control\`, \`progress-tracking\` | +| \`/toh-protect\` | \`engineer-harness\` | +| \`/toh-help\` | (none — self-contained; run \`.toh/commands/toh-help.md\` directly) | + +### Core Skills (Always Available) +- \`memory-system\` - Memory read/write protocol +- \`engineer-harness\` - Smart tool selection + human-friendly reporting + next steps +- \`smart-routing\` - Command routing logic + +### Loading Protocol: +1. User types /toh-[command] +2. Read required skill files from \`.toh/skills/[skill-name]/SKILL.md\` +3. Execute following skill instructions +4. Save memory after completion + +### ⚠️ NEVER Skip Skills! +Skills contain CRITICAL best practices, design tokens, and rules. + +## 🔒 Skills Loading Checkpoint (REQUIRED) + +> **ENFORCEMENT:** You MUST report skills loaded at the START of your response! + +### Required Response Start: + +\`\`\`markdown +📚 **Skills Loaded:** +- skill-name-1 ✅ (brief what you learned) +- skill-name-2 ✅ (brief what you learned) + +🤖 **Agent:** agent-name + +💾 **Memory:** Loaded ✅ + +--- + +[Then continue with your work...] +\`\`\` + +### Why This Matters: +- If you don't report skills → You didn't read them +- If you skip skills → Output quality drops significantly +- Skills have design tokens, patterns, and critical rules +- This checkpoint proves you followed the protocol + +## Skills Reference + +All skills are in \`.toh/skills/\` (Central Resources): +- \`vibe-orchestrator\` - Core methodology +- \`ui-first-builder\` - UI patterns +- \`dev-engineer\` - TypeScript, State, Forms +- \`design-craft\` - Design system, anti-patterns & business-appropriate fit +- \`premium-experience\` - Premium multi-page apps +- \`test-engineer\` - Testing with Playwright +- \`backend-engineer\` - Supabase integration +- \`platform-specialist\` - LINE, Mobile, Desktop +- \`memory-system\` - Memory protocol +- \`engineer-harness\` - Smart tool selection, reporting & next steps +- \`debug-protocol\` - Debugging guide +- \`error-handling\` - Error handling patterns + +## Getting Started + +Start with: +\`\`\` +/toh-vibe [describe what system you want] +\`\`\` + +The AI will: +1. Analyze your requirements +2. Break down into tasks +3. Create UI with English mock data +4. Add logic and state management +5. Polish the design +6. Deliver production-ready code + +--- + +**GitHub:** https://github.com/wasintoh/toh-framework +**Author:** Wasin Treesinthuros (Innovation Vantage) + + +`; +} + +function generateAgentsMdTH(agentRoster, ide = 'codex', probedSubagents = false) { + const rt = AGENTS_MD_RUNTIMES[ide] || AGENTS_MD_RUNTIMES.codex; + return ` +# 🎯 Toh Framework + +> **"Type Once, Have it all!"** - AI-Orchestration Driven Development +> **"Command once, done without questions"** + +## Project Memory + +${rt.memoryTH} + +This file is a compact index. Full specs live on disk and MUST be read at runtime: +- Commands → \`.toh/commands/toh-.md\` +- Agents → \`.toh/agents/.md\` +- Skills → \`.toh/skills//SKILL.md\` + +## Identity + +You are **Toh Framework Agent** - AI that helps Solo Developers build SaaS by themselves + +${renderCapabilitiesSection(ide)} + +${runtimeIdentityLine(rt, probedSubagents)} + +## Core Philosophy (AODD - AI-Orchestration Driven Development) + +1. **Human Language → Tasks** - User commands naturally, you break into tasks +2. **Orchestrator → Agents** - Call relevant agents to work automatically +3. **User doesn't handle process** - No questions, no waiting, just complete it +4. **Test → Fix → Loop** - Test, fix, until pass + +## Tech Stack (Do not change!) + +| Category | Technology | +|------|----------| +| Framework | Next.js 16 (App Router) | +| Styling | Tailwind CSS + shadcn/ui | +| State | Zustand | +| Forms | React Hook Form + Zod | +| Backend | Supabase | +| Testing | Playwright | +| Language | TypeScript (strict) | + +## Language Rules + +- **Response Language:** Match user's language (if unsure, use Thai) +- **UI Labels/Buttons:** Thai (Save, Cancel, Dashboard) +- **Mock Data:** Thai names, addresses, phone numbers +- **Code Comments:** Thai allowed +- **Validation Messages:** Thai + +If user types in English, respond in English + +## 🚨 Command Handling (Very Important!) + +> **You must remember and execute these commands immediately!** +> When user types any pattern below, treat it as a direct command +> The table below is an INDEX only — when a command is invoked, read +> \`.toh/commands/toh-.md\` (e.g. \`.toh/commands/toh-vibe.md\`) and follow it. +> That file is the command's full behavior spec. + +| Command | Shortcuts (ALL VALID) | Purpose | +|---------|----------------------|---------| +| \`/toh-help\` | \`/toh-h\`, \`toh help\`, \`toh h\` | Show all commands | +| \`/toh-plan\` | \`/toh-p\`, \`toh plan\`, \`toh p\` | 🧠 **THE BRAIN** — writes .toh/plan.md, one approval, then builds autonomously | +| \`/toh-vibe\` | \`/toh-v\`, \`toh vibe\`, \`toh v\` | Create new project - UI + Logic + Mock Data | +| \`/toh-ui\` | \`/toh-u\`, \`toh ui\`, \`toh u\` | Create UI - Pages, Components, Layouts | +| \`/toh-dev\` | \`/toh-d\`, \`toh dev\`, \`toh d\` | Add Logic - TypeScript, Zustand, Forms | +| \`/toh-design\` | \`/toh-ds\`, \`toh design\`, \`toh ds\` | Polish Design - Make it beautiful, not AI-looking | +| \`/toh-test\` | \`/toh-t\`, \`toh test\`, \`toh t\` | Test system - Auto test & fix until pass | +| \`/toh-connect\` | \`/toh-c\`, \`toh connect\`, \`toh c\` | Connect Backend - Supabase, Auth, RLS | +| \`/toh-line\` | \`/toh-l\`, \`toh line\`, \`toh l\` | LINE MINI App - convert (LIFF SDK) | +| \`/toh-mobile\` | \`/toh-m\`, \`toh mobile\`, \`toh m\` | Mobile App - PWA / Capacitor | +| \`/toh-fix\` | \`/toh-f\`, \`toh fix\`, \`toh f\` | Fix Bug - Debug and fix issues | +| \`/toh-ship\` | \`/toh-s\`, \`toh ship\`, \`toh s\` | Deploy - Vercel, Production ready | +| \`/toh-protect\` | \`/toh-pt\`, \`toh protect\`, \`toh pt\` | 🔐 Security Audit - Full security check | + +### ⚡ Execution Rules: + +1. **Remember Immediately** - See \`/toh-\` or \`toh \` = command! +2. **Read the command file first** - \`.toh/commands/toh-.md\` is the full spec; never execute from this index alone +3. **Check Description** - Does command have description after? + - ✅ **Has description** → Execute immediately, no confirmation + - ❓ **No description** → Introduce yourself as that command's agent and ask first (e.g. "I'm **Vibe Agent** 🎨, what system would you like me to create?"). Exception: \`/toh-help\` always runs immediately +4. **Follow Memory Protocol** - Read/write \`.toh/memory/\` + +## Memory System (Automatic, 7 files — Tiered Loading) + +Toh Framework has Memory system at \`.toh/memory/\`. Read only what the task needs: +- **Tier 1 (ALWAYS read, ~800 tokens):** \`active.md\` (current task) + \`summary.md\` (project overview) +- **Tier 2 (per task type):** \`architecture.md\` + \`components.md\` for build/code work; \`changelog.md\` for debug work +- **Tier 3 (only when referenced):** \`decisions.md\` (past decisions) + \`agents-log.md\` (agent activity) +- \`archive/\` - Historical data (load when needed) + +## 🚨 Required: Memory Protocol (Tiered Loading) + +> **Important:** Must follow this every time! Never read all 7 files by reflex. + +### Before Starting Work: +1. Check if \`.toh/memory/\` folder exists +2. Read Tier 1: \`.toh/memory/active.md\` + \`.toh/memory/summary.md\` +3. Read Tier 2 for this task type (build/code → \`architecture.md\` + \`components.md\`; debug → \`changelog.md\`) +4. Read Tier 3 (\`decisions.md\`, \`agents-log.md\`) ONLY when referenced +5. If files empty but code exists → Analyze project first! +6. Tell User: "Memory loaded! [brief summary]" + +### After Completing Work (write per relevance): +1. Update \`.toh/memory/active.md\` - ALWAYS (What was done, next steps) +2. Update \`.toh/memory/summary.md\` - when the project shape changes (feature done / new structure) +3. Update \`.toh/memory/architecture.md\` / \`components.md\` - when modules/stores/hooks/utils change +4. Update \`.toh/memory/changelog.md\` + \`agents-log.md\` - record the change and which agent did it +5. Update \`.toh/memory/decisions.md\` - if a real decision was made +6. Tell User: "Memory saved ✅" + +### ⚠️ Important Rules: +- Never start work without reading Tier 1 (active.md + summary.md)! +- Never finish work without updating active.md! +- Read Tier 2 / Tier 3 only when the task type or a reference calls for it! +- Memory files must always be in English! + +## Rules to Follow + +1. **No Basic Questions** - Decide yourself +2. **Use Fixed Tech Stack** - Don't change +3. **Respond in Thai** - All communication in Thai +4. **Thai Mock Data** - Use Thai names, addresses, phone numbers +5. **UI First** - Build UI first to visualize +6. **Production Ready** - Not a prototype + +## Mock Data Examples + +Use realistic Thai data: +- First names: Somchai, Somying, Manee, Mana +- Last names: Jaidee, Rakrian, Suksun +- Addresses: Bangkok, Chiang Mai, Phuket +- Phone: 081-234-5678 +- Email: somchai@example.com + +## Agents (roster — full specs in \`.toh/agents/\`) + +${agentRoster} + +This table is a summary ONLY. Before acting as (or delegating to) any agent, +read \`.toh/agents/.md\` — the agent's workflow, rules, tool limits, and +quality bar live in that file, not here. + +## 🚨 Required: Load Skills & Agents + +> **Important:** Before executing any /toh- command, must load related skills! + +### Command → Skills Map + +| Command | Load These Skills (from \`.toh/skills/\`) | +|--------|-------------------------------------------| +| \`/toh\` | \`smart-routing\`, \`orchestration-protocol\`, \`engineer-harness\` | +| \`/toh-vibe\` | \`vibe-orchestrator\`, \`orchestration-protocol\`, \`premium-experience\`, \`design-craft\`, \`ui-first-builder\`, \`engineer-harness\` | +| \`/toh-ui\` | \`ui-first-builder\`, \`design-craft\`, \`engineer-harness\` | +| \`/toh-dev\` | \`dev-engineer\`, \`backend-engineer\`, \`engineer-harness\` | +| \`/toh-design\` | \`design-craft\`, \`premium-experience\` | +| \`/toh-test\` | \`test-engineer\`, \`debug-protocol\`, \`error-handling\` | +| \`/toh-connect\` | \`backend-engineer\`, \`integrations\` | +| \`/toh-plan\` | \`plan-orchestrator\`, \`orchestration-protocol\`, \`business-context\`, \`smart-routing\`, \`engineer-harness\` | +| \`/toh-fix\` | \`debug-protocol\`, \`error-handling\`, \`test-engineer\` | +| \`/toh-line\` | \`platform-specialist\`, \`integrations\` | +| \`/toh-mobile\` | \`platform-specialist\`, \`ui-first-builder\` | +| \`/toh-ship\` | \`version-control\`, \`progress-tracking\` | +| \`/toh-protect\` | \`engineer-harness\` | +| \`/toh-help\` | (none — self-contained; run \`.toh/commands/toh-help.md\` directly) | + +### Core Skills (Always Available) +- \`memory-system\` - Memory system +- \`engineer-harness\` - Smart tool selection + human-friendly reporting + next steps +- \`smart-routing\` - Command routing + +### Loading Steps: +1. User types /toh-[command] +2. Read skill files from \`.toh/skills/[skill-name]/SKILL.md\` +3. Execute according to skill instructions +4. Save memory after completion + +### ⚠️ Never Skip Skills! +Skills contain best practices, design tokens, and important rules + +## 🔒 Skills Loading Checkpoint (Required) + +> **Required:** Must report loaded skills at the beginning of response! + +### Response Start Format: + +\`\`\`markdown +📚 **Skills Loaded:** +- skill-name-1 ✅ (brief summary of what was loaded) +- skill-name-2 ✅ (brief summary of what was loaded) + +🤖 **Agent:** agent name + +💾 **Memory:** loaded ✅ + +--- + +[then continue with work...] +\`\`\` + +### Why This Is Required: +- If skills not reported → means not read +- If skills skipped → work quality will decrease significantly +- Skills contain design tokens, patterns, and important rules +- This checkpoint proves protocol compliance + +## Skills Reference + +All skills are located at \`.toh/skills/\` (Central Resources): +- \`vibe-orchestrator\` - Core methodology +- \`ui-first-builder\` - UI patterns +- \`dev-engineer\` - TypeScript, State, Forms +- \`design-craft\` - Design system, anti-patterns & business-appropriate fit +- \`premium-experience\` - Premium multi-page apps +- \`test-engineer\` - Testing with Playwright +- \`backend-engineer\` - Supabase integration +- \`platform-specialist\` - LINE, Mobile, Desktop +- \`memory-system\` - Memory protocol +- \`engineer-harness\` - Smart tool selection, reporting & next steps + +## Getting Started + +Start with: +\`\`\` +/toh-vibe [describe the system you want] +\`\`\` + +AI will: +1. Analyze requirements +2. Break down tasks +3. Create UI with Thai mock data +4. Add logic and state management +5. Polish design to look beautiful +6. Deliver production-ready code + +--- + +**GitHub:** https://github.com/wasintoh/toh-framework +**Author:** Wasin Treesinthuros (Innovation Vantage) + + +`; } diff --git a/installer/ide-handlers/shared.js b/installer/ide-handlers/shared.js index db09431..1f902ed 100644 --- a/installer/ide-handlers/shared.js +++ b/installer/ide-handlers/shared.js @@ -58,17 +58,14 @@ export const CAPABILITY_PROFILES = { }, codex: { ide: 'codex', - client: 'codex-cli', - detection: 'declared-at-install', - subagents: 'native', + subagents: 'none', teams: false, goal: false, loop: false, hooks: false, - workflows: 'native', - parallel: true, - modelRouting: true, - nativeAgents: true + workflows: false, + parallel: false, + modelRouting: false }, 'gemini-cli': { ide: 'gemini-cli', @@ -322,10 +319,9 @@ export function renderCapabilitiesSection(ide) { const profile = CAPABILITY_PROFILES[key]; const display = IDE_DISPLAY[key] || { name: key || 'unknown runtime', contextFile: 'this context file' }; - // Unknown IDE -> conservative profile. Unknown is not a reason to disable a - // capability supplied by a client that the installer cannot probe. + // Unknown IDE -> conservative sequential profile const p = profile || { - ide: key, subagents: 'unknown', teams: false, goal: false, + ide: key, subagents: 'none', teams: false, goal: false, loop: false, hooks: false, workflows: false, parallel: false, modelRouting: false }; @@ -334,14 +330,12 @@ export function renderCapabilitiesSection(ide) { 'Never guess your runtime; it is stated here.'; const lines = []; - if (p.subagents === 'native' && key === 'codex') { - lines.push('- Native subagents: YES — delegate via `.codex/agents/*.toml` (parallel only for independent tasks on disjoint files, max 4 concurrent)'); - } else if (p.subagents === 'native') { + if (p.subagents === 'native') { + // Claude Code keeps its original wording (Task tool); Cursor's native + // subagents live in .cursor/agents/ and have no Task tool. lines.push(key === 'cursor' ? '- Native subagents: YES — delegate to the Toh specialists in `.cursor/agents/*.md` (fall back to sequential execution in this session when delegation is unavailable)' : '- Native subagents: YES — delegate via the Agent tool (Task) (parallel only for independent tasks on disjoint files, max 4 concurrent)'); - } else if (p.subagents === 'unknown') { - lines.push('- Native subagents: UNKNOWN — keep native delegation eligible; do not disable it from an unavailable probe'); } else if (p.subagents === 'file-based') { lines.push('- Native subagents: file-based — `.agents/agents/.md` (`subagent: true`), delegate via `invoke_subagent`; fall back to sequential execution in this session when delegation is unavailable'); } else { @@ -375,23 +369,9 @@ export function renderCapabilitiesSection(ide) { if (p.commands) { lines.push('- Slash commands: YES — `/toh-*` load natively from `.agents/commands/` (the same 14 commands also exist as skills)'); } - if (p.workflows === 'version-gated') { - lines.push('- Workflows: version-gated (Claude Code >= 2.1.154)'); - } else if (p.workflows === 'native') { - lines.push('- Workflows: YES — invoke installed native skills with `$toh-*`'); - } else if (p.workflows === true) { - lines.push('- Workflows: YES — `/toh-*` workflows in `.agents/workflows/` (legacy mirror: `.agent/workflows/`)'); - } else { - lines.push('- Workflows: NO'); - } lines.push(p.modelRouting - ? key === 'codex' - ? '- Model routing: YES — native agent TOML sets Codex model and reasoning per Toh role' - : '- Model routing: YES — haiku = scaffold/tests · sonnet = builders · opus = planning/QC' + ? '- Model routing: YES — haiku = scaffold/tests · sonnet = builders · opus = planning/QC' : '- Model routing: NO — ignore model tiers and proceed'); - if (p.nativeAgents) { - lines.push('- Native agent files: `.codex/agents/*.toml` — generated from `.toh/agents/*.md` with ownership-safe updates'); - } if (!p.parallel) { lines.push(p.subagents === 'none' ? '- Execution mode: run THE TOH LOOP **sequentially in this session** (orchestration-protocol skill); recovery = checkbox-resume from `.toh/plan.md`' @@ -514,16 +494,6 @@ export async function writeAgentsSkills(targetDir, srcDir) { // ---- (b) toh-* command skills converted from the TOML prompts ------ for (const { file, name } of await collectCommandTomls(srcDir)) { - // Codex's native handler owns command wrappers when it has already - // generated one. Keep that content and its Codex manifest hash intact; - // the shared writer still fills the same directory for other runtimes. - const existingCommandPath = join(outDir, name, 'SKILL.md'); - if (await fs.pathExists(existingCommandPath)) { - const existing = await fs.readFile(existingCommandPath, 'utf8'); - const { fm } = splitFrontmatter(existing); - if (fm?.metadata?.generator === 'toh-framework' && fm.metadata.kind === 'command') continue; - } - const parsed = await renderCommandPrompt(file, 'writeAgentsSkills'); const description = parsed.description; // A skill is invoked by name, not by a slash command with arguments, so diff --git a/installer/install.js b/installer/install.js index cea94f3..151f978 100644 --- a/installer/install.js +++ b/installer/install.js @@ -13,9 +13,8 @@ import { dirname, join } from 'path'; import { setupClaudeCode } from './ide-handlers/claude-code.js'; import { setupCursor } from './ide-handlers/cursor.js'; import { setupGeminiCLI } from './ide-handlers/gemini-cli.js'; -import { CODEX_AGENTS_DIR, CODEX_SKILLS_DIR, setupCodex, uninstallCodex } from './ide-handlers/codex.js'; import { setupAntigravityCLI } from './ide-handlers/antigravity-cli.js'; -import { writeAgentsMd } from './ide-handlers/codex.js'; +import { setupCodex, writeAgentsMd } from './ide-handlers/codex.js'; import { setupZcode } from './ide-handlers/zcode.js'; import { transformCommand, writeCapabilitiesJson, writeAgentsSkills, seedFileIfAbsent } from './ide-handlers/shared.js'; @@ -23,12 +22,6 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const SRC_DIR = join(__dirname, '..', 'src'); -// ora corrupts the node:test child-process IPC channel (Node 24), so tests set -// TOH_QUIET=1 to silence spinners. Default CLI behavior is unchanged. -function startSpinner(text) { - return ora({ text, isSilent: process.env.TOH_QUIET === '1' }).start(); -} - // Read version from package.json (Single Source of Truth) const PKG_PATH = join(__dirname, '..', 'package.json'); const pkg = await fs.readJson(PKG_PATH); @@ -41,7 +34,9 @@ const VERSION = pkg.version; // math divide by zero and loop forever inside stop()/succeed(). On a // zero-width TTY fall back to plain non-animated output (isEnabled: false). const spin = (text) => { - const options = { text, discardStdin: false }; + // TOH_QUIET=1: no spinner output at all (the in-band test runner sets it — + // ora's stream writes corrupt node:test's IPC channel on Node 24). + const options = { text, discardStdin: false, isSilent: process.env.TOH_QUIET === '1' }; if (process.stderr.isTTY && !(process.stderr.columns > 0)) { options.isEnabled = false; // zero-width pty: plain output, no animation } @@ -204,29 +199,29 @@ export async function install(options) { // Check for existing installation const existingInstall = await checkExistingInstall(config.targetDir); - if (existingInstall) { - // --quick must stay non-interactive: a reinstall defaults to Quick Update. - let action = 'update'; - if (!quick) { - ({ action } = await inquirer.prompt([{ - type: 'list', - name: 'action', - message: 'Existing Toh Framework installation detected. What would you like to do?', - choices: [ - { name: '🔄 Quick Update (preserve customizations)', value: 'update' }, - { name: '🗑️ Fresh Install (overwrite all)', value: 'fresh' }, - { name: '❌ Cancel', value: 'cancel' } - ] - }])); - } else { - console.log(chalk.cyan(' ↻ Existing files found — updating in place (customizations preserved).')); - } + if (existingInstall && quick) { + // v2.1: --quick means "no questions". Reinstalling on top of leftovers is + // the documented path back after `toh uninstall` (which keeps plan.md, + // progress.md and the memory folders), so default to the safe branch — + // update in place, preserving whatever the user still has. + console.log(chalk.cyan(' ↻ Existing files found — updating in place (customizations preserved).')); + } else if (existingInstall) { + const { action } = await inquirer.prompt([{ + type: 'list', + name: 'action', + message: 'Existing Toh Framework installation detected. What would you like to do?', + choices: [ + { name: '🔄 Quick Update (preserve customizations)', value: 'update' }, + { name: '🗑️ Fresh Install (overwrite all)', value: 'fresh' }, + { name: '❌ Cancel', value: 'cancel' } + ] + }]); if (action === 'cancel') { console.log(chalk.yellow('\nInstallation cancelled.')); return; } - + if (action === 'fresh') { await cleanExistingInstall(config.targetDir); } @@ -307,7 +302,7 @@ export async function install(options) { // allowProbedSubagents: the AGENTS.md Runtime Identity sentence may // reflect probed codex subagents ONLY when ZCode is nowhere in play // (AGENTS.md has two readers; never claim what only one has). - await setupIDEWithSpinner('Codex CLI', () => + await setupIDEWithSpinner('Codex (CLI + desktop app)', () => setupCodex(config.targetDir, SRC_DIR, config.language, { allowProbedSubagents: !zcodeInPlay })); break; case 'zcode': @@ -391,7 +386,7 @@ async function promptConfiguration(defaults) { { name: 'Claude Code (Anthropic)', value: 'claude', checked: true }, { name: 'Cursor', value: 'cursor', checked: false }, { name: 'Antigravity CLI (agy) — Google', value: 'antigravity', checked: false }, - { name: 'Codex CLI — OpenAI', value: 'codex', checked: false }, + { name: 'Codex (CLI + desktop app) — OpenAI', value: 'codex', checked: false }, { name: 'ZCode (Z.ai)', value: 'zcode', checked: false } ], validate: (input) => input.length > 0 ? true : 'Please select at least one IDE' @@ -473,18 +468,16 @@ async function installAgentsSkills(config) { async function checkExistingInstall(targetDir) { const markers = [ join(targetDir, '.toh'), - join(targetDir, '.agents', 'skills'), - join(targetDir, '.codex', 'config.toml'), join(targetDir, '.claude', 'skills', 'vibe-orchestrator'), join(targetDir, '.cursor', 'rules', 'toh-framework.mdc') ]; - + return markers.some(marker => fs.existsSync(marker)); } async function cleanExistingInstall(targetDir) { const spinner = spin('Cleaning existing installation...').start(); - + // Fresh Install is the explicitly destructive, user-confirmed path: removing // .toh wipes .toh/memory + plan.md + progress.md, and .claude/memory goes too // so a fresh install genuinely resets memory (issue #2). @@ -501,11 +494,7 @@ async function cleanExistingInstall(targetDir) { await fs.remove(p); } } - - // Codex: remove TOH-managed .agents/skills + the AGENTS.md TOH block, - // preserving any user skills and user AGENTS.md content. - await uninstallCodex(targetDir); - + spinner.succeed('Cleaned existing installation'); } @@ -517,7 +506,19 @@ async function setupIDEWithSpinner(ideName, setupFn) { spinner.succeed(`${ideName} configured (${configFile})`); } catch (error) { spinner.fail(`Failed to configure ${ideName}: ${error.message}`); - throw error; + // Hard budget violations (error.fatal, e.g. the Codex 24 KiB AGENTS.md + // block or the Antigravity 12,000-char Always-On rule) mean the generated + // output would be silently broken — never report success past them. + if (error.fatal) { + console.error(chalk.red( + `\n✖ Installation aborted: ${ideName} failed a hard size-budget check (see above). ` + + `Fix the generator and re-run the installer.\n` + )); + // Thrown (not process.exit) so bin/toh-cli.js sets the exit code and the + // test suite can observe the abort. `reported` stops a second message. + error.reported = true; + throw error; + } } } @@ -527,7 +528,7 @@ function getIDEConfigFile(ideName) { 'Cursor': '.cursor/rules/*.mdc', 'Antigravity CLI (agy)': '.agents/rules/toh-framework.md', 'Gemini CLI (legacy)': '.gemini/GEMINI.md', - 'Codex CLI': `${CODEX_SKILLS_DIR}/ + ${CODEX_AGENTS_DIR}/ + AGENTS.md + .codex/config.toml`, + 'Codex (CLI + desktop app)': 'AGENTS.md', 'ZCode (Z.ai)': 'AGENTS.md + .agents/' }; return configs[ideName] || 'configured'; @@ -585,6 +586,7 @@ async function countFiles(dir) { async function generateManifest(config, inventory = null) { const spinner = spin('Generating manifest...').start(); + const manifest = { version: VERSION, installedAt: new Date().toISOString(), @@ -993,11 +995,15 @@ function printNextSteps(config) { } if (config.ides.includes('codex') || config.ides.includes('codex-cli')) { - console.log(row(chalk.white(pad(' Codex CLI:')))); - console.log(row(chalk.green(' $toh-vibe') + chalk.gray(' - Native skill: new project'.padEnd(47)))); - console.log(row(chalk.green(' /skills') + chalk.gray(' - Browse all TOH skills'.padEnd(49)))); - console.log(row(chalk.green(` ${CODEX_SKILLS_DIR}/`) + chalk.gray(' - 14 workflow skills installed'.padEnd(42)))); - console.log(row(chalk.green(` ${CODEX_AGENTS_DIR}/`) + chalk.gray(' - 8 native agents installed'.padEnd(42)))); + console.log(row(chalk.white(pad(' Codex (CLI + desktop app):')))); + // 9 chars green + 51 chars gray = 60 + console.log(row(chalk.green(' codex') + chalk.gray(' - Start Codex CLI in project'.padEnd(51)))); + // 13 chars green + 47 chars gray = 60 + console.log(row(chalk.green(' $toh-vibe') + chalk.gray(' - Create new project (native skill)'.padEnd(47)))); + // 11 chars green + 49 chars gray = 60 + console.log(row(chalk.green(' /skills') + chalk.gray(' - Browse all 14 /toh-* skills'.padEnd(49)))); + // 18 chars green + 42 chars gray = 60 + console.log(row(chalk.green(' .codex/agents/') + chalk.gray(' - 8 native Toh agents'.padEnd(42)))); console.log(empty); } diff --git a/installer/uninstall.js b/installer/uninstall.js index 5988994..951f7c5 100644 --- a/installer/uninstall.js +++ b/installer/uninstall.js @@ -37,7 +37,7 @@ import yaml from 'js-yaml'; import crypto from 'crypto'; import os from 'os'; import { fileURLToPath } from 'url'; -import { dirname, join, resolve, basename, parse as parsePath } from 'path'; +import { dirname, join, resolve, relative, basename, parse as parsePath } from 'path'; import { generateClaudeMd } from './ide-handlers/claude-code.js'; import { uninstallCodex } from './ide-handlers/codex.js'; @@ -212,8 +212,7 @@ async function buildCatalog(srcDir) { templateFiles: [], workflowFiles: [], geminiCommandFiles: [], - agentsSkillDirs: [], - agentsCommandFiles: [] + agentsSkillDirs: [] }; if (!(await fs.pathExists(srcDir))) return cat; @@ -282,7 +281,8 @@ function derivedPaths(cat) { } for (const d of cat.agentsSkillDirs) add(`.agents/skills/${d}/SKILL.md`); for (const f of cat.agentsCommandFiles) add(`.agents/commands/${f}`); - for (const n of cat.agentNames) add(`.codex/agents/${n.replace(/\.md$/, '.toml')}`); + // v2.2: native Codex agents + their ownership manifest + for (const n of cat.agentNames) add(`.codex/agents/${n.replace(/\.md$/, '')}.toml`); add('.codex/toh-framework.json'); for (const f of cat.workflowFiles) { add(`.agents/workflows/${f}`); @@ -481,7 +481,7 @@ async function buildPlan(ctx) { await planStopHook(ctx, plan, '.claude/settings.json', 'the Toh "keep going" reminder'); await planStopHook(ctx, plan, '.agents/hooks.json', 'the Toh "keep going" reminder'); await planStampedFile(ctx, plan, '.claude/loop.md', TFW_LOOP_MARKER, 'first line'); - await planCodexConfig(ctx, plan); + await planStampedFile(ctx, plan, '.codex/config.toml', CODEX_CONFIG_STAMP, 'first line'); await planHashOnlyFile(ctx, plan, '.cursorrules'); await planHashOnlyFile(ctx, plan, '.gemini/settings.json'); await planGeminiMd(ctx, plan); @@ -907,41 +907,6 @@ async function planStampedFile(ctx, plan, rel, stamp, whereLabel) { } } -/** .codex/config.toml — remove only the Toh marker block from shared config. */ -async function planCodexConfig(ctx, plan) { - const { targetDir } = ctx; - const rel = '.codex/config.toml'; - if (!(await isRealFile(targetDir, rel))) return; - if (!(await pathIsSafe(targetDir, rel))) { - plan.warnings.push({ rel, why: 'this path is a shortcut (symlink) — I left it alone' }); - return; - } - - const text = await readText(targetDir, rel).catch(() => ''); - const start = '# TOH-FRAMEWORK-START'; - const end = '# TOH-FRAMEWORK-END'; - const starts = text.split(start).length - 1; - const ends = text.split(end).length - 1; - if (starts === 1 && ends === 1) { - const block = new RegExp(`[ \\t]*${start}\\r?\\n[\\s\\S]*?[ \\t]*${end}[ \\t]*\\r?\\n?`); - const out = text.replace(block, '').trim(); - const sha = await shaOfFile(targetDir, rel); - plan.edits.push({ - rel, - group: 'codex', - kind: out ? 'write' : 'delete', - ...(out ? { content: `${out}\\n` } : {}), - expectedSha: sha, - summary: out ? 'take out only the Toh Codex settings block — your settings stay' : 'delete it — it contains only Toh Codex settings' - }); - if (!out) markDirs(plan, rel); - return; - } - - // Older main-generated configs use the historical first-line stamp. - await planStampedFile(ctx, plan, rel, CODEX_CONFIG_STAMP, 'first line'); -} - /** * Files with no marker at all that the installer OVERWRITES (.cursorrules, * .gemini/settings.json). The only honest ownership test is "the bytes are @@ -1397,28 +1362,56 @@ export async function uninstall(options = {}) { return 1; } - // Codex has a native ownership manifest separate from the general install - // inventory. Keep the feature branch's per-IDE command working while the - // default path below retains main's complete ownership planner. + // --- per-IDE removal (v2.2: Codex only) ------------------------------------ + // `--ide codex` removes just the native agent files this installer wrote + // (hash-verified) plus their manifest. AGENTS.md and .codex/config.toml are + // shared surfaces (ZCode reads AGENTS.md too) and stay — run without --ide + // for the complete, previewed removal. if (requestedIdes) { const isCodex = (ide) => ide === 'codex' || ide === 'codex-cli'; if (!requestedIdes.every(isCodex)) { const unsupported = requestedIdes.filter((ide) => !isCodex(ide)); console.log(chalk.yellow( - `Per-IDE uninstall is currently implemented for Codex CLI only (got: ${unsupported.join(', ')}).\n` + + `Per-IDE removal currently supports Codex only (got: ${unsupported.join(', ')}).\n` + `Run without --ide for a full uninstall, or use --ide codex.\n` )); return 1; } - const result = await uninstallCodex(targetDir, { - dryRun, - backup: options.backup !== false - }); - const removed = result.removedSkills.length + result.removedAgents.length; + // Same manners as the full path: preview first, then one question. + const preview = await uninstallCodex(targetDir, { dryRun: true }); + const keptNote = preview.keptAgents.length + ? ` ${plural(preview.keptAgents.length, 'file', 'files')} you edited will stay: ${preview.keptAgents.join(', ')}.` + : ''; + const staysNote = 'AGENTS.md, .codex/config.toml and .toh/ stay (shared surfaces — run without --ide to remove everything).'; + if (preview.removedAgents.length === 0) { + console.log(chalk.yellow(`No Toh-written native agent files found in .codex/agents/ — nothing to remove.${keptNote}\n${staysNote}\n`)); + return 0; + } + console.log(chalk.white( + `I will remove ${plural(preview.removedAgents.length, 'native agent file', 'native agent files')} from .codex/agents/ ` + + `(${preview.removedAgents.join(', ')}) plus .codex/toh-framework.json.${keptNote}\n${staysNote}\n` + )); + if (dryRun) { + console.log(chalk.gray('This was a preview (--dry-run). Nothing was deleted or changed.\n')); + return 0; + } + if (!assumeYes) { + let go = false; + try { + ({ go } = await inquirer.prompt([{ type: 'confirm', name: 'go', message: 'Go ahead?', default: false }])); + } catch { + console.log(chalk.yellow('\nI could not read your answer, so I stopped and changed nothing.\n')); + return 1; + } + if (!go) { + console.log(chalk.yellow('\nStopped. Nothing was changed.\n')); + return 0; + } + } + const result = await uninstallCodex(targetDir, { dryRun: false, backup: options.backup !== false }); console.log(chalk.green( - dryRun - ? `Codex CLI preview: ${removed} native file(s) would be removed; .toh/ state stays untouched.\n` - : `Codex CLI removed: ${removed} native file(s); .toh/ state stays untouched.\n` + `Codex: removed ${plural(result.removedAgents.length, 'native agent file', 'native agent files')} from .codex/agents/.` + + (result.backupPath ? ` A copy was saved to ${relative(targetDir, result.backupPath)}/ first.` : '') + '\n' )); return 0; } diff --git a/package.json b/package.json index b7b34e0..2278620 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "toh-framework", "version": "2.1.1", "type": "module", - "description": "AI-Orchestration Driven Development - Type Once, Have it all! Approve once and the TOH LOOP builds, tests, and fixes a whole app until verified DONE. For Claude Code, Cursor, Antigravity (+ Antigravity CLI), Codex CLI, and ZCode.", + "description": "AI-Orchestration Driven Development - Type Once, Have it all! Approve once and the TOH LOOP builds, tests, and fixes a whole app until verified DONE. For Claude Code, Cursor, Antigravity (+ Antigravity CLI), Codex (CLI + desktop app), and ZCode.", "author": { "name": "Wasin Treesinthuros", "email": "dr.wasin@gmail.com" diff --git a/src/agents/README.md b/src/agents/README.md index 2295397..b3dfbb9 100644 --- a/src/agents/README.md +++ b/src/agents/README.md @@ -17,7 +17,7 @@ src/agents/*.md ← single source (superset frontmatter + canonical │ (uses native name / description / tools / model) ├── Cursor → native subagents → .cursor/agents/*.md ├── Antigravity → .agents/agents/*.md (subagent: true) - ├── Codex CLI → native agents in .codex/agents/ + command skills in .agents/skills/ + ├── Codex → compact roster in AGENTS.md + native agents in .codex/agents/*.toml ├── ZCode → compact roster in AGENTS.md, bodies read from .toh/ └── Gemini (legacy) → convert frontmatter to the Gemini format ``` @@ -38,7 +38,7 @@ tools: # Claude Code: native tool allowlist - Edit - Bash model: sonnet # Claude Code: model tier per agent -modelIntent: implementation # Codex: lightweight | implementation | planning | review +modelIntent: implementation # Codex: lightweight | implementation | planning | review -> reasoning effort (model is inherited) skills: # Toh skill bindings (all IDEs) - ui-first-builder - design-craft @@ -82,7 +82,7 @@ The same source produces IDE-appropriate output at install time: | Claude Code | `.claude/agents/*.md` | Copied as-is (native `name`/`description`/`tools`/`model`) | | Cursor (2.4+) | `.cursor/agents/*.md` | Native subagents (`readonly` derived from the tools allowlist) | | Antigravity (+ CLI) | `.agents/agents/*.md` | Frontmatter converted, `subagent: true` | -| Codex CLI | `.codex/agents/*.toml` + `.agents/skills/*/SKILL.md` + `AGENTS.md` | 8 native agents + 14 workflow skills; 23 supporting skills stay in `.toh/` | +| Codex (CLI + desktop app) | `AGENTS.md` roster + `.codex/agents/*.toml` | Compact roster table + one native agent per Toh agent (no `model` key; effort from `modelIntent`) | | ZCode | `AGENTS.md` roster + `.toh/agents/*.md` | Compact roster table; bodies read at runtime | | Gemini CLI (legacy) | `.toh/agents/*.md` | Frontmatter converted per IDE | @@ -99,8 +99,6 @@ The same source produces IDE-appropriate output at install time: ``` There is no `subagents/` folder anymore — the installer is the transform layer. -Codex model names and reasoning are selected centrally from each agent's -`modelIntent`; Claude's `model` tier remains for Claude Code compatibility. --- diff --git a/src/commands/toh-help.md b/src/commands/toh-help.md index e985309..577e4b3 100644 --- a/src/commands/toh-help.md +++ b/src/commands/toh-help.md @@ -146,7 +146,7 @@ Every response from Toh includes: - 📚 **23 Skills** - Including Orchestration Protocol & Security Engineer - 🎨 **Design Identity** - Per-project DESIGN.md design identity + versioned AVOID-LIST - 📦 **15 Component Templates** - Ready-to-use premium components -- 🌐 **6 IDEs** - Claude Code, Cursor, Antigravity (+ Antigravity CLI), Codex CLI, ZCode, Gemini CLI (legacy) +- 🌐 **6 IDEs** - Claude Code, Cursor, Antigravity (+ Antigravity CLI), Codex (CLI + desktop app), ZCode, Gemini CLI (legacy) --- @@ -168,7 +168,7 @@ Every response from Toh includes: | Claude Code | `CLAUDE.md` | | Cursor | `.cursor/rules/*.mdc` | | Antigravity CLI (agy) + IDE | `.agents/` — rules, skills, `.agents/workflows/` (legacy: `.agent/workflows/`) | -| Codex CLI | `AGENTS.md` + `.agents/skills/` + `.codex/agents/` | +| Codex (CLI + desktop app) | `AGENTS.md` | | ZCode (Z.ai) | `AGENTS.md` + `.agents/` — skills and `/toh-*` commands | | Gemini CLI (legacy, `--legacy-gemini`) | `.gemini/GEMINI.md` | diff --git a/src/memory/MEMORY-SYSTEM.md b/src/memory/MEMORY-SYSTEM.md index e81dd10..3929090 100644 --- a/src/memory/MEMORY-SYSTEM.md +++ b/src/memory/MEMORY-SYSTEM.md @@ -218,6 +218,6 @@ Memory system works identically across: - ✅ Antigravity (+ Antigravity CLI) - ✅ ZCode (Z.ai) - ✅ Gemini CLI (legacy) -- ✅ Codex CLI +- ✅ Codex (CLI + desktop app) Same files, same format, same behavior! diff --git a/src/skills/orchestration-protocol/SKILL.md b/src/skills/orchestration-protocol/SKILL.md index f653ee0..d866cb4 100644 --- a/src/skills/orchestration-protocol/SKILL.md +++ b/src/skills/orchestration-protocol/SKILL.md @@ -39,7 +39,7 @@ Your runtime identity is declared by the platform context file that loaded you: | `.agents/rules/toh-framework.md` | Antigravity (+ Antigravity CLI) | | `GEMINI.md` | Gemini CLI (legacy) | -Confirm capabilities from `.toh/capabilities.json` (written by the installer). If it is missing, infer conservatively: Claude Code has subagents, teams, hooks, `/goal`, `/loop`; Cursor (2.4+) and Antigravity have native/file-based subagents but no teams; Codex CLI has native custom agents and workflow skills; ZCode and legacy Gemini are single-session sequential. An unknown capability probe never disables a client-native feature. +Confirm capabilities from `.toh/capabilities.json` (written by the installer). If it is missing, infer conservatively: Claude Code has subagents, teams, hooks, `/goal`, `/loop`; Cursor (2.4+) and Antigravity have native/file-based subagents but no teams; Codex and ZCode are single-session sequential. ### Step 2 — Runtime probe (ONLY for what install time cannot know) @@ -60,7 +60,7 @@ Do NOT invent other detection heuristics. Identity comes from Step 1; the probe Three rungs, best first. **Each rung: if unavailable, fall back one rung.** Sequential is the floor and is always available. 1. **AGENT TEAMS** — Claude Code with the teams env flag set, AND the plan has >= 3 independent modules plus a QC role. Recipe in Section F. If unavailable, fall back one rung. -2. **NATIVE SUBAGENTS** — the Agent tool (Task) or Codex CLI's `.codex/agents/*.toml` is available. Delegate tasks to TFW agents; parallel only under the rules below. If unavailable, fall back one rung. +2. **NATIVE SUBAGENTS** — the Agent tool (Task) exists. Delegate tasks to TFW agents; parallel only under the rules below. If unavailable, fall back one rung. 3. **SEQUENTIAL SELF** — execute every task yourself, in order, in this session. This is the default mode and the correct choice more often than not. ### When to use which @@ -70,9 +70,7 @@ Three rungs, best first. **Each rung: if unavailable, fall back one rung.** Sequ | <= 3 tasks total | SEQUENTIAL | | Same-file or dependent edits | SEQUENTIAL | | Debugging / fixing | SEQUENTIAL | -| Cursor / Antigravity with native subagents | NATIVE SUBAGENTS when the task is independent; otherwise SEQUENTIAL | -| Codex CLI with native agents | NATIVE SUBAGENTS | -| Runtime without subagents (ZCode / Gemini) | SEQUENTIAL | +| Runtime without subagents (Codex / ZCode / Gemini) | SEQUENTIAL | | >= 2 independent tasks on disjoint files, each substantial (~5+ min) | PARALLEL subagents | | MVP-scale: >= 3 independent modules + a QC role, teams flag set | TEAMS | @@ -110,7 +108,7 @@ Mirrors TFW agent frontmatter; teams and subagents both honor per-agent `model` | **sonnet** | Builders — ui-builder, dev-builder, implementation work | | **opus** | Planning, QC/review, design review | -On runtimes without model routing, ignore this table and proceed. Codex native agents use the generated `model` and `model_reasoning_effort` fields; do not map Claude tier names at runtime. +On runtimes without model routing, ignore this table and proceed. --- @@ -229,7 +227,6 @@ Section E is the floor on every runtime. On Claude Code the installer ships mach | **Workflows** (>= 2.1.154, optional) | `/toh-sweep` (not shipped — optional pattern you can save to `.claude/workflows/`) can fan out fixers per failing task until checks pass. | **Antigravity** runs the same loop and also gets a deterministic Stop hook (`.agents/hooks.json`) that blocks ending a session while `.toh/plan.md` has unchecked tasks (both hooks exempt a terminal-status plan). **Every other runtime** (Cursor / Codex / ZCode / Gemini) runs the SAME loop as prose in one session — no hooks, no `/goal`. The recovery mechanism there is checkbox-resume: a fresh session picks up at the first unchecked task — unless the plan header carries a terminal status (done/draft/blocked/paused), which is reported instead of resumed. If context runs low mid-plan, flush state (plan checkboxes + progress.md + active.md pointer), then tell the user to re-run the command — it resumes exactly where it stopped. -**Codex CLI** runs the same loop and may delegate independent tasks to generated native agents; its parent still owns checkpoint verification and checkbox updates. **Every other runtime** (Cursor / ZCode / Gemini) runs the SAME loop as prose in one session — no hooks, no `/goal`. The recovery mechanism there is checkbox-resume: a fresh session picks up at the first unchecked task — unless the plan header carries a terminal status (done/draft/blocked/paused), which is reported instead of resumed. If context runs low mid-plan, flush state (plan checkboxes + progress.md + active.md pointer), then tell the user to re-run the command — it resumes exactly where it stopped. --- diff --git a/src/skills/progress-tracking/SKILL.md b/src/skills/progress-tracking/SKILL.md index e7fb5a2..60d7854 100644 --- a/src/skills/progress-tracking/SKILL.md +++ b/src/skills/progress-tracking/SKILL.md @@ -342,7 +342,7 @@ Progress is saved in `.toh/progress.md`, so it syncs across: - Claude Code - Cursor - Antigravity (+ Antigravity CLI) -- Codex CLI +- Codex (CLI + desktop app) - ZCode (Z.ai) - Gemini CLI (legacy) diff --git a/src/skills/smart-routing/SKILL.md b/src/skills/smart-routing/SKILL.md index 7c51029..f98776c 100644 --- a/src/skills/smart-routing/SKILL.md +++ b/src/skills/smart-routing/SKILL.md @@ -143,8 +143,7 @@ Choose from the **execution ladder in `orchestration-protocol` (Section B)** — - **Claude Code** → ladder: teams > subagents > sequential - **Cursor (2.4+)** → native subagents in `.cursor/agents/`, one task at a time - **Antigravity** → file-based subagents via `invoke_subagent`, one task at a time -- **Codex CLI** → native agents for independent tasks; sequential TOH LOOP for dependent work -- **ZCode / Gemini (legacy)** → sequential TOH LOOP in-session +- **Codex / ZCode / Gemini (legacy)** → sequential TOH LOOP in-session --- diff --git a/tests/codex.test.js b/tests/codex.test.js index 70dc17c..b3ada55 100644 --- a/tests/codex.test.js +++ b/tests/codex.test.js @@ -1,24 +1,28 @@ /** - * Codex integration tests (node:test). + * Codex integration tests (node:test) — native agents in .codex/agents/*.toml. + * + * Every check below is an observable outcome of a real `install()` run into a + * temp directory, never a re-statement of the generator's own constants. * * Covers: - * - fresh installation layout (.toh/, .agents/skills/, AGENTS.md) - * - preservation of existing AGENTS.md content - * - idempotency + deterministic output across reinstalls - * - preservation of unrelated user Codex skills - * - removal of stale TOH-managed skills - * - uninstall (TOH files out, user files stay) - * - SKILL.md validity (frontmatter, non-empty body, resolving references) - * - AGENTS.md size guard and Codex project-doc quota + * - fresh install layout (.toh/, shared .agents/skills/, .codex/agents/, AGENTS.md) + * - agent TOML shape: no `model` key (inherits the session), reasoning effort + * from modelIntent, read-only sandbox for read-only agents + * - single writer for .agents/skills/ — same wrappers whatever the IDE order + * - a user's existing .codex/config.toml is never modified + * - ownership by hash: edited or user-created agent files are never touched + * - AGENTS.md: user content preserved, reinstall idempotent, block under budget + * - `uninstall --ide codex` (native agents only) and the full uninstall + * - the codex capability profile stays the conservative, probe-upgraded floor * * Run: npm test */ -// Silence ora spinners: ora corrupts the node:test child-process IPC channel. -// Reads at call time, so import hoisting is not a problem. +// Silence ora spinners: ora's stream writes corrupt node:test's child-process +// IPC channel on Node 24. Read at call time, so import hoisting is harmless. process.env.TOH_QUIET = '1'; -import { test, before, after } from 'node:test'; +import { test } from 'node:test'; import assert from 'node:assert/strict'; import { createHash } from 'node:crypto'; import fs from 'fs-extra'; @@ -29,53 +33,32 @@ import yaml from 'js-yaml'; import { parse as parseToml } from 'smol-toml'; import { install } from '../installer/install.js'; +import { uninstall } from '../installer/uninstall.js'; import { - AGENTS_MAX_BYTES, CODEX_AGENTS_DIR, - CODEX_MODEL_ROUTING, - CODEX_SKILLS_DIR, - assertAgentsMdSize, - setupCodex, - uninstallCodex, - installCodexSkills, + CODEX_MANIFEST_PATH, + CODEX_REASONING_EFFORT, + installCodexAgents, readAgentCatalog, - readCommandCatalog, - readSupportingSkillCatalog, - setupCodexConfig, - translateAgentToCodex + resolveCodexModelIntent, + translateAgentToCodex, + uninstallCodex, + writeAgentsMd } from '../installer/ide-handlers/codex.js'; -import { CAPABILITY_PROFILES, renderCapabilitiesSection } from '../installer/ide-handlers/shared.js'; +import { CAPABILITY_PROFILES } from '../installer/ide-handlers/shared.js'; const __filename = fileURLToPath(import.meta.url); const REPO_ROOT = path.join(path.dirname(__filename), '..'); const SRC_DIR = path.join(REPO_ROOT, 'src'); const EXPECTED_COMMANDS = [ - 'toh', - 'toh-connect', - 'toh-design', - 'toh-dev', - 'toh-fix', - 'toh-help', - 'toh-line', - 'toh-mobile', - 'toh-plan', - 'toh-protect', - 'toh-ship', - 'toh-test', - 'toh-ui', - 'toh-vibe' + 'toh', 'toh-connect', 'toh-design', 'toh-dev', 'toh-fix', 'toh-help', 'toh-line', + 'toh-mobile', 'toh-plan', 'toh-protect', 'toh-ship', 'toh-test', 'toh-ui', 'toh-vibe' ]; const EXPECTED_AGENTS = [ - 'backend-connector', - 'design-reviewer', - 'dev-builder', - 'plan-orchestrator', - 'platform-adapter', - 'root-cause-debugger', - 'test-runner', - 'ui-builder' + 'backend-connector', 'design-reviewer', 'dev-builder', 'plan-orchestrator', + 'platform-adapter', 'root-cause-debugger', 'test-runner', 'ui-builder' ]; // ---------------------------------------------------------------- helpers @@ -84,12 +67,16 @@ async function makeTmpProject() { return fs.mkdtemp(path.join(os.tmpdir(), 'toh-codex-test-')); } -async function quickInstallCodex(targetDir) { - // install() with quick: true is fully non-interactive, even on reinstall. - await install({ target: targetDir, ide: 'codex', quick: true }); +/** install() with quick: true is fully non-interactive, even on reinstall. */ +async function quickInstall(targetDir, ide = 'codex') { + await install({ target: targetDir, ide, quick: true }); } -/** Map of relative file path -> content for every file under root. */ +function sha256(buf) { + return createHash('sha256').update(buf).digest('hex'); +} + +/** Map of relative path -> content for every file under root. */ async function snapshotTree(root) { const out = new Map(); const walk = async (dir) => { @@ -103,478 +90,325 @@ async function snapshotTree(root) { return out; } -function parseSkillFrontmatter(raw) { +function frontmatterOf(raw) { const m = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/); - assert.ok(m, 'SKILL.md must start with a YAML frontmatter block'); - return { fm: yaml.load(m[1]), body: m[2] }; + assert.ok(m, 'file must start with a YAML frontmatter block'); + return { fm: yaml.load(m[1]) || {}, body: m[2] }; } -// ---------------------------------------------------------------- tests +async function readAgentToml(dir, name) { + const raw = await fs.readFile(path.join(dir, CODEX_AGENTS_DIR, `${name}.toml`), 'utf8'); + return { raw, parsed: parseToml(raw) }; +} -test('fresh install creates .toh/, .agents/skills/, config.toml and AGENTS.md', async () => { - const dir = await makeTmpProject(); - try { - await quickInstallCodex(dir); - - assert.ok(await fs.pathExists(path.join(dir, '.toh', 'plan.md')), '.toh/plan.md exists'); - assert.ok(await fs.pathExists(path.join(dir, '.toh', 'progress.md')), '.toh/progress.md exists'); - assert.ok(await fs.pathExists(path.join(dir, '.toh', 'memory', 'active.md')), 'memory seeded'); - assert.ok(await fs.pathExists(path.join(dir, '.toh', 'skills', 'orchestration-protocol', 'SKILL.md')), '.toh skills installed'); - assert.ok(await fs.pathExists(path.join(dir, 'AGENTS.md')), 'AGENTS.md exists'); - assert.ok(await fs.pathExists(path.join(dir, '.codex', 'config.toml')), 'Codex config exists'); - const config = await fs.readFile(path.join(dir, '.codex', 'config.toml'), 'utf8'); - const parsedConfig = parseToml(config); - assert.equal(parsedConfig.project_doc_max_bytes, 65536, 'project doc quota is a root-level setting'); - assert.equal(parsedConfig.features.multi_agent, true, 'native delegation is enabled'); - assert.ok(!(await fs.pathExists(path.join(dir, '.codex', 'skills'))), 'legacy .codex/skills is not used'); - - const generatedAgents = (await fs.readdir(path.join(dir, CODEX_AGENTS_DIR))).sort(); - assert.deepEqual(generatedAgents, EXPECTED_AGENTS.map((name) => `${name}.toml`), 'all eight native agents generated'); - const manifest = await fs.readJson(path.join(dir, '.codex', 'toh-framework.json')); - assert.equal(Object.keys(manifest.agents).length, 8, 'native agents are ownership-tracked'); - const rootCause = parseToml(await fs.readFile(path.join(dir, CODEX_AGENTS_DIR, 'root-cause-debugger.toml'), 'utf8')); - assert.equal(rootCause.sandbox_mode, 'read-only', 'read-only source agent maps to Codex read-only sandbox'); - assert.equal(rootCause.model, CODEX_MODEL_ROUTING.review.model); - assert.equal(rootCause.model_reasoning_effort, CODEX_MODEL_ROUTING.review.model_reasoning_effort); - assert.ok(rootCause.developer_instructions.includes('Status, Result, Evidence, Files, and Blockers')); - assert.ok((await fs.readFile(path.join(dir, 'AGENTS.md'), 'utf8')).includes('.codex/agents/*.toml')); - - const supporting = await readSupportingSkillCatalog(SRC_DIR); - assert.equal(supporting.length, 23, 'all 23 supporting skills are catalogued'); - assert.equal((await fs.readdir(path.join(dir, CODEX_SKILLS_DIR))).length, 37, '23 supporting skills and 14 workflow commands are wrapped'); - for (const skill of EXPECTED_COMMANDS) { - assert.ok( - await fs.pathExists(path.join(dir, CODEX_SKILLS_DIR, skill, 'SKILL.md')), - `native skill installed: ${skill}` - ); - } - } finally { - await fs.remove(dir); - } -}); +// ---------------------------------------------------------------- tests -test('native agent translation preserves source intents and valid TOML', async () => { +test('fresh Codex install creates .toh/, shared .agents/skills/, .codex/agents/ and AGENTS.md', async () => { const dir = await makeTmpProject(); try { - await quickInstallCodex(dir); - const catalog = await readAgentCatalog(dir); - assert.deepEqual(catalog.map((agent) => agent.name), EXPECTED_AGENTS); - for (const agent of catalog) { - const parsed = parseToml(translateAgentToCodex(agent)); - assert.equal(parsed.name, agent.name); - assert.equal(parsed.model, CODEX_MODEL_ROUTING[agent.modelIntent].model); - assert.equal(parsed.model_reasoning_effort, CODEX_MODEL_ROUTING[agent.modelIntent].model_reasoning_effort); - assert.equal(typeof parsed.developer_instructions, 'string'); - } - } finally { - await fs.remove(dir); - } -}); + await quickInstall(dir); -test('Codex CLI capabilities are native without a CLI binary probe', () => { - const profile = CAPABILITY_PROFILES.codex; - assert.equal(profile.client, 'codex-cli'); - assert.equal(profile.detection, 'declared-at-install'); - assert.equal(profile.subagents, 'native'); - assert.equal(profile.parallel, true); - assert.equal(profile.modelRouting, true); - const section = renderCapabilitiesSection('codex'); - assert.match(section, /\.codex\/agents/); - assert.match(section, /Codex model and reasoning/); - assert.doesNotMatch(section, /single-session only/); -}); + assert.ok(await fs.pathExists(path.join(dir, '.toh', 'plan.md'))); + assert.ok(await fs.pathExists(path.join(dir, '.toh', 'memory', 'active.md'))); -test('existing Codex feature and quota settings are not duplicated', async () => { - const dir = await makeTmpProject(); - try { - const configPath = path.join(dir, '.codex', 'config.toml'); - const userConfig = 'project_doc_max_bytes = 12345\n[features]\nmulti_agent = false\n'; - await fs.ensureDir(path.dirname(configPath)); - await fs.writeFile(configPath, userConfig); + // The shared writer owns .agents/skills/: 23 framework skills + 14 command skills. + const skillDirs = (await fs.readdir(path.join(dir, '.agents', 'skills'))).sort(); + assert.equal(skillDirs.length, 37); + for (const cmd of EXPECTED_COMMANDS) assert.ok(skillDirs.includes(cmd), `missing ${cmd}`); - await setupCodexConfig(dir); - assert.equal(await fs.readFile(configPath, 'utf8'), userConfig, 'user-owned Codex settings stay unchanged'); - } finally { - await fs.remove(dir); - } -}); + // One native Codex agent per Toh agent, plus the ownership manifest. + const tomls = (await fs.readdir(path.join(dir, CODEX_AGENTS_DIR))).sort(); + assert.deepEqual(tomls, EXPECTED_AGENTS.map((n) => `${n}.toml`)); + const manifest = await fs.readJson(path.join(dir, CODEX_MANIFEST_PATH)); + assert.equal(manifest.generator, 'toh-framework'); + assert.equal(Object.keys(manifest.agents).length, EXPECTED_AGENTS.length); -test('existing Codex features keep their value while root quota is added', async () => { - const dir = await makeTmpProject(); - try { - const configPath = path.join(dir, '.codex', 'config.toml'); - const userConfig = '[features]\nmulti_agent = false\n'; - await fs.ensureDir(path.dirname(configPath)); - await fs.writeFile(configPath, userConfig); - - await setupCodexConfig(dir); - const parsed = parseToml(await fs.readFile(configPath, 'utf8')); - assert.equal(parsed.project_doc_max_bytes, 65536); - assert.equal(parsed.features.multi_agent, false, 'user feature value stays unchanged'); - } finally { - await fs.remove(dir); - } -}); + const agentsMd = await fs.readFile(path.join(dir, 'AGENTS.md'), 'utf8'); + assert.match(agentsMd, //); + assert.match(agentsMd, /\.codex\/agents\/\*\.toml/); + assert.match(agentsMd, /\$toh-/); -test('root quota is inserted before unrelated Codex tables', async () => { - const dir = await makeTmpProject(); - try { - const configPath = path.join(dir, '.codex', 'config.toml'); - const userConfig = '[profiles.default]\nmodel = "user-model"\n'; - await fs.ensureDir(path.dirname(configPath)); - await fs.writeFile(configPath, userConfig); - - await setupCodexConfig(dir); - const parsed = parseToml(await fs.readFile(configPath, 'utf8')); - assert.equal(parsed.project_doc_max_bytes, 65536, 'quota remains at the TOML root'); - assert.equal(parsed.features.multi_agent, true); - assert.equal(parsed.profiles.default.model, 'user-model', 'unrelated table is preserved'); + // project-doc quota is written only because no config.toml existed. + const config = await fs.readFile(path.join(dir, '.codex', 'config.toml'), 'utf8'); + assert.equal(parseToml(config).project_doc_max_bytes, 131072); + assert.doesNotMatch(config, /\[features\]/); } finally { await fs.remove(dir); } }); -test('Claude agent output keeps native fields and drops Codex-only modelIntent', async () => { +test('agent TOML: valid, no model key, effort from intent, sandbox from tool allowlist', async () => { const dir = await makeTmpProject(); try { - await install({ target: dir, ide: 'claude-code', quick: true }); - const raw = await fs.readFile(path.join(dir, '.claude', 'agents', 'ui-builder.md'), 'utf8'); - const { fm } = parseSkillFrontmatter(raw); - assert.equal(fm.name, 'ui-builder'); - assert.equal(fm.model, 'sonnet'); - assert.equal(fm.modelIntent, undefined); - } finally { - await fs.remove(dir); - } -}); + await quickInstall(dir); + const agents = await readAgentCatalog(dir); + assert.equal(agents.length, EXPECTED_AGENTS.length); -test('existing AGENTS.md user content is preserved', async () => { - const dir = await makeTmpProject(); - try { - const userContent = '# My Project\n\nKeep this text.\n'; - await fs.writeFile(path.join(dir, 'AGENTS.md'), userContent); + for (const agent of agents) { + const { raw, parsed } = await readAgentToml(dir, agent.name); + assert.equal(parsed.name, agent.name); + assert.ok(parsed.description.length > 0); + assert.ok(parsed.developer_instructions.length > 100, `${agent.name}: instructions look empty`); + // The model is inherited from the parent session — never pinned here. + assert.equal(Object.hasOwn(parsed, 'model'), false, `${agent.name}: must not pin a model`); + assert.doesNotMatch(raw, /^model\s*=/m); + assert.equal(parsed.model_reasoning_effort, CODEX_REASONING_EFFORT[agent.modelIntent]); + assert.ok(['read-only', 'workspace-write'].includes(parsed.sandbox_mode)); + } - await quickInstallCodex(dir); + // Read-only allowlist (Read/Grep/Glob/Bash) -> read-only sandbox; builders write. + assert.equal((await readAgentToml(dir, 'root-cause-debugger')).parsed.sandbox_mode, 'read-only'); + assert.equal((await readAgentToml(dir, 'ui-builder')).parsed.sandbox_mode, 'workspace-write'); - const agents = await fs.readFile(path.join(dir, 'AGENTS.md'), 'utf8'); - assert.ok(agents.includes('Keep this text.'), 'user text kept'); - assert.ok(agents.includes(''), 'TOH block present'); - assert.ok(agents.indexOf('Keep this text.') < agents.indexOf(''), 'TOH block appended after user content'); + // Intents declared in src/agents/*.md land where expected. + assert.equal((await readAgentToml(dir, 'plan-orchestrator')).parsed.model_reasoning_effort, 'high'); + assert.equal((await readAgentToml(dir, 'test-runner')).parsed.model_reasoning_effort, 'low'); + assert.equal((await readAgentToml(dir, 'dev-builder')).parsed.model_reasoning_effort, 'medium'); } finally { await fs.remove(dir); } }); -test('reinstall is idempotent and deterministic', async () => { - const dir = await makeTmpProject(); - try { - await quickInstallCodex(dir); - const first = await snapshotTree(dir); - - await quickInstallCodex(dir); // quick mode must not prompt - const second = await snapshotTree(dir); - - // Exactly one TOH block in AGENTS.md - const agents = second.get('AGENTS.md'); - const starts = agents.split('').length - 1; - const ends = agents.split('').length - 1; - assert.equal(starts, 1, 'exactly one TOH start marker'); - assert.equal(ends, 1, 'exactly one TOH end marker'); - - // Same file set (no duplicated skills) - assert.deepEqual([...first.keys()].sort(), [...second.keys()].sort(), 'file set stable'); - - // Deterministic content for everything except timestamped install metadata - const VOLATILE = new Set(['.toh/manifest.json', '.toh/capabilities.json']); - for (const [file, content] of first) { - if (VOLATILE.has(file)) continue; - assert.equal(second.get(file), content, `identical content: ${file}`); - } - } finally { - await fs.remove(dir); - } +test('modelIntent resolution: explicit key wins, Claude tier is the fallback', () => { + assert.equal(resolveCodexModelIntent({ modelIntent: 'review', model: 'haiku' }), 'review'); + assert.equal(resolveCodexModelIntent({ modelIntent: 'deep_reasoning' }), 'planning'); + assert.equal(resolveCodexModelIntent({ modelIntent: 'scaffold' }), 'lightweight'); + assert.equal(resolveCodexModelIntent({ model: 'haiku' }), 'lightweight'); + assert.equal(resolveCodexModelIntent({ model: 'opus' }), 'planning'); + assert.equal(resolveCodexModelIntent({ model: 'sonnet' }), 'implementation'); + assert.equal(resolveCodexModelIntent({}), 'implementation'); + assert.equal(resolveCodexModelIntent({ modelIntent: 'nonsense', model: 'opus' }), 'planning'); }); -test('native agents preserve user and ZCode files and modified generated agents', async () => { - const dir = await makeTmpProject(); - try { - const userAgentPath = path.join(dir, CODEX_AGENTS_DIR, 'user-agent.toml'); - const zcodePath = path.join(dir, '.zcode', 'agents', 'user.toml'); - const userAgent = 'name = "user-agent"\ndescription = "User-owned"\n'; - await fs.ensureDir(path.dirname(userAgentPath)); - await fs.ensureDir(path.dirname(zcodePath)); - await fs.writeFile(userAgentPath, userAgent); - await fs.writeFile(zcodePath, 'user zcode configuration\n'); - - await quickInstallCodex(dir); - const generatedPath = path.join(dir, CODEX_AGENTS_DIR, 'ui-builder.toml'); - const modified = `${await fs.readFile(generatedPath, 'utf8')}\nUser customization.\n`; - await fs.writeFile(generatedPath, modified); - - await quickInstallCodex(dir); - assert.equal(await fs.readFile(generatedPath, 'utf8'), modified, 'modified generated agent is preserved'); - assert.equal(await fs.readFile(userAgentPath, 'utf8'), userAgent, 'user Codex agent is preserved'); - assert.equal(await fs.readFile(zcodePath, 'utf8'), 'user zcode configuration\n', 'ZCode files are untouched'); - - const result = await uninstallCodex(dir, { backup: false }); - assert.equal(result.removedAgents.length, 7, 'only unmodified generated agents are removed'); - assert.ok(await fs.pathExists(generatedPath), 'modified generated agent remains'); - assert.ok(await fs.pathExists(userAgentPath), 'user Codex agent remains'); - assert.ok(await fs.pathExists(zcodePath), 'ZCode file remains'); - } finally { - await fs.remove(dir); - } +test('translateAgentToCodex escapes arbitrary bodies into valid TOML', () => { + const toml = translateAgentToCodex({ + name: 'edge-case', + description: 'Quotes "here", backslash \\ and a tab\t.', + body: 'Line one\n\nLine "two" with `code` and \\ backslashes\n# not a toml comment', + tools: ['Read', 'Write'], + skills: ['design-craft'], + triggers: ['ui', 'design'], + modelIntent: 'implementation', + maxTurns: 12 + }); + const parsed = parseToml(toml); + assert.equal(parsed.name, 'edge-case'); + assert.equal(parsed.sandbox_mode, 'workspace-write'); + assert.match(parsed.developer_instructions, /Line "two" with `code`/); + assert.match(parsed.developer_instructions, /Source turn budget hint: 12\./); + assert.match(parsed.developer_instructions, /\.toh\/skills\/design-craft\/SKILL\.md/); }); -test('unrelated user Codex skills are never touched', async () => { - const dir = await makeTmpProject(); +test('single writer: .agents/skills wrappers are identical regardless of IDE order', async () => { + const a = await makeTmpProject(); + const b = await makeTmpProject(); try { - const userSkillDir = path.join(dir, CODEX_SKILLS_DIR, 'my-company-skill'); - await fs.ensureDir(userSkillDir); - const userSkill = '---\nname: my-company-skill\ndescription: mine\n---\n\nUser-owned.\n'; - await fs.writeFile(path.join(userSkillDir, 'SKILL.md'), userSkill); - - await quickInstallCodex(dir); - - assert.equal( - await fs.readFile(path.join(userSkillDir, 'SKILL.md'), 'utf8'), - userSkill, - 'user skill content unchanged' - ); - - // Even a user-owned skill with a toh- name survives (no ownership hash). - const userTohDir = path.join(dir, CODEX_SKILLS_DIR, 'toh-custom'); - await fs.ensureDir(userTohDir); - const userToh = '---\nname: toh-custom\ndescription: user owned\n---\n\nMine.\n'; - await fs.writeFile(path.join(userTohDir, 'SKILL.md'), userToh); - await setupCodex(dir, SRC_DIR, 'en'); - assert.equal(await fs.readFile(path.join(userTohDir, 'SKILL.md'), 'utf8'), userToh, 'user toh-* skill kept'); + await quickInstall(a, 'codex,cursor'); // Codex first + await quickInstall(b, 'cursor'); // Cursor first… + await quickInstall(b, 'codex'); // …Codex added later + + const skillsA = await snapshotTree(path.join(a, '.agents', 'skills')); + const skillsB = await snapshotTree(path.join(b, '.agents', 'skills')); + assert.equal(skillsA.size, 37); + assert.deepEqual([...skillsA.keys()].sort(), [...skillsB.keys()].sort()); + for (const [rel, content] of skillsA) { + assert.equal(skillsB.get(rel), content, `${rel} differs by install order`); + } + + // Wrappers are the shared, runtime-neutral ones — nothing Codex-specific + // leaks into what Cursor / Antigravity / ZCode read. + const vibe = skillsA.get(path.join('toh-vibe', 'SKILL.md')); + assert.doesNotMatch(vibe, /\.codex\/agents/); + assert.doesNotMatch(vibe, /Codex has no Toh Stop hook/); + const { fm } = frontmatterOf(vibe); + assert.equal(fm.name, 'toh-vibe'); + assert.ok(fm.description); + + // Codex got all 8 native agents in both orders. + for (const dir of [a, b]) { + const tomls = await fs.readdir(path.join(dir, CODEX_AGENTS_DIR)); + assert.equal(tomls.length, EXPECTED_AGENTS.length, `${dir}: expected 8 native agents`); + } } finally { - await fs.remove(dir); + await fs.remove(a); + await fs.remove(b); } }); -test('stale TOH-managed skills are removed on reinstall', async () => { +test('an existing user .codex/config.toml is never modified', async () => { const dir = await makeTmpProject(); try { - await quickInstallCodex(dir); - - // Simulate a skill generated by an older TOH version with a manifest entry. - const staleDir = path.join(dir, CODEX_SKILLS_DIR, 'toh-legacy'); - await fs.ensureDir(staleDir); - const staleContent = '---\nname: toh-legacy\ndescription: old\nmetadata:\n generator: toh-framework\n---\n\nOld.\n'; - await fs.writeFile( - path.join(staleDir, 'SKILL.md'), - staleContent - ); - const staleAgentPath = path.join(dir, CODEX_AGENTS_DIR, 'toh-legacy-agent.toml'); - const staleAgentContent = 'name = "toh-legacy-agent"\ndescription = "old"\ndeveloper_instructions = "old"\n'; - await fs.writeFile(staleAgentPath, staleAgentContent); - const manifestPath = path.join(dir, '.codex', 'toh-framework.json'); - const manifest = await fs.readJson(manifestPath); - manifest.files['.agents/skills/toh-legacy/SKILL.md'] = { - sha256: createHash('sha256').update(staleContent).digest('hex'), - kind: 'command', - source: '.toh/commands/toh-legacy.md' - }; - manifest.agents['.codex/agents/toh-legacy-agent.toml'] = { - sha256: createHash('sha256').update(staleAgentContent).digest('hex'), - source: '.toh/agents/toh-legacy-agent.md', - modelIntent: 'implementation' - }; - await fs.writeJson(manifestPath, manifest, { spaces: 2 }); + const configPath = path.join(dir, '.codex', 'config.toml'); + const userConfig = 'model = "gpt-5.6-sol"\napproval_policy = "on-request"\n\n[mcp_servers.github]\ncommand = "gh-mcp"\n'; + await fs.outputFile(configPath, userConfig); + const before = sha256(await fs.readFile(configPath)); - await setupCodex(dir, SRC_DIR, 'en'); + await quickInstall(dir); + await quickInstall(dir); // and again, on reinstall - assert.ok(!(await fs.pathExists(staleDir)), 'stale TOH-managed skill removed'); - assert.ok(!(await fs.pathExists(staleAgentPath)), 'stale TOH-managed agent removed'); - assert.ok(await fs.pathExists(path.join(dir, CODEX_SKILLS_DIR, 'toh-plan', 'SKILL.md')), 'current skills intact'); + assert.equal(sha256(await fs.readFile(configPath)), before, 'user config.toml was rewritten'); + assert.equal(await fs.readFile(configPath, 'utf8'), userConfig); } finally { await fs.remove(dir); } }); -test('uninstall removes TOH Codex files and keeps user files + .toh state', async () => { +test('ownership by hash: edited and user-created agent files are never touched', async () => { const dir = await makeTmpProject(); try { - // Pre-existing user content - await fs.writeFile(path.join(dir, 'AGENTS.md'), '# My Project\n\nKeep this text.\n'); - const userSkillDir = path.join(dir, CODEX_SKILLS_DIR, 'my-company-skill'); - await fs.ensureDir(userSkillDir); - await fs.writeFile(path.join(userSkillDir, 'SKILL.md'), '---\nname: my-company-skill\ndescription: mine\n---\n'); - - await quickInstallCodex(dir); - - // Pretend the loop ran: live state that must survive a codex uninstall. - await fs.writeFile(path.join(dir, '.toh', 'plan.md'), '# Plan: real work\n\n- [ ] T001 [P] ui-builder — thing in app/page.tsx\n'); - - const { removedSkills, removedAgents, agentsMd, config, backupPath } = await uninstallCodex(dir); - - assert.equal(removedSkills.length, 14, 'all TOH workflow wrappers removed'); - assert.equal(removedAgents.length, 8, 'all unmodified native agents removed'); - for (const skill of EXPECTED_COMMANDS) { - assert.ok(!(await fs.pathExists(path.join(dir, CODEX_SKILLS_DIR, skill))), `removed ${skill}`); - } - assert.ok(await fs.pathExists(path.join(userSkillDir, 'SKILL.md')), 'user skill remains'); - assert.ok(await fs.pathExists(path.join(dir, CODEX_SKILLS_DIR)), '.agents/skills dir kept'); - assert.ok(!(await fs.pathExists(path.join(dir, CODEX_AGENTS_DIR))), 'empty native agent directory removed'); - assert.equal(config, 'removed'); - assert.ok(backupPath && await fs.pathExists(backupPath), 'uninstall creates a backup'); - - assert.equal(agentsMd, 'updated'); - const agents = await fs.readFile(path.join(dir, 'AGENTS.md'), 'utf8'); - assert.ok(agents.includes('Keep this text.'), 'user AGENTS.md text remains'); - assert.ok(!agents.includes(''), 'TOH block removed'); - - // Codex-only uninstall must not touch shared .toh state. - const plan = await fs.readFile(path.join(dir, '.toh', 'plan.md'), 'utf8'); - assert.ok(plan.includes('real work'), '.toh/plan.md preserved'); + await quickInstall(dir); + + // User edits one generated agent and adds their own. + const edited = path.join(dir, CODEX_AGENTS_DIR, 'ui-builder.toml'); + const editedContent = (await fs.readFile(edited, 'utf8')) + '\n# my tweak\n'; + await fs.writeFile(edited, editedContent); + const own = path.join(dir, CODEX_AGENTS_DIR, 'my-reviewer.toml'); + const ownContent = 'name = "my-reviewer"\ndescription = "mine"\ndeveloper_instructions = "keep"\n'; + await fs.writeFile(own, ownContent); + + const result = await installCodexAgents(dir); + assert.deepEqual(result.kept, ['ui-builder']); + assert.equal(result.installed.length, EXPECTED_AGENTS.length - 1); + assert.equal(await fs.readFile(edited, 'utf8'), editedContent, 'edited agent was overwritten'); + assert.equal(await fs.readFile(own, 'utf8'), ownContent, 'user agent was touched'); + + // The manifest still remembers the original hash for the edited file, so a + // later uninstall knows it is no longer ours. + const manifest = await fs.readJson(path.join(dir, CODEX_MANIFEST_PATH)); + assert.ok(manifest.agents['.codex/agents/ui-builder.toml']); + assert.equal(manifest.agents['.codex/agents/my-reviewer.toml'], undefined); } finally { await fs.remove(dir); } }); -test('uninstall deletes AGENTS.md only when it was TOH-only', async () => { +test('stale agent files we wrote are removed on reinstall only when untouched', async () => { const dir = await makeTmpProject(); try { - await setupCodex(dir, SRC_DIR, 'en'); // standalone: creates AGENTS.md - const { agentsMd } = await uninstallCodex(dir); - assert.equal(agentsMd, 'removed'); - assert.ok(!(await fs.pathExists(path.join(dir, 'AGENTS.md'))), 'TOH-only AGENTS.md removed'); - assert.ok(!(await fs.pathExists(path.join(dir, '.codex', 'config.toml'))), 'TOH-only config removed'); + await quickInstall(dir); + const manifestPath = path.join(dir, CODEX_MANIFEST_PATH); + const manifest = await fs.readJson(manifestPath); + + // Simulate an agent that existed in an older release: write it and record it. + const staleOurs = 'name = "old-agent"\ndescription = "gone upstream"\ndeveloper_instructions = "x"\n'; + await fs.writeFile(path.join(dir, CODEX_AGENTS_DIR, 'old-agent.toml'), staleOurs); + manifest.agents['.codex/agents/old-agent.toml'] = { sha256: sha256(staleOurs), source: '.toh/agents/old-agent.md' }; + const staleEdited = 'name = "old-edited"\ndescription = "edited"\ndeveloper_instructions = "y"\n'; + await fs.writeFile(path.join(dir, CODEX_AGENTS_DIR, 'old-edited.toml'), staleEdited + '# changed\n'); + manifest.agents['.codex/agents/old-edited.toml'] = { sha256: sha256(staleEdited), source: '.toh/agents/old-edited.md' }; + await fs.writeJson(manifestPath, manifest, { spaces: 2 }); + + await installCodexAgents(dir); + assert.equal(await fs.pathExists(path.join(dir, CODEX_AGENTS_DIR, 'old-agent.toml')), false, 'untouched stale file should go'); + assert.equal(await fs.pathExists(path.join(dir, CODEX_AGENTS_DIR, 'old-edited.toml')), true, 'edited stale file must stay'); } finally { await fs.remove(dir); } }); -test('every generated SKILL.md is valid and its references resolve', async () => { +test('AGENTS.md: user content preserved, reinstall idempotent, block under budget', async () => { const dir = await makeTmpProject(); try { - await quickInstallCodex(dir); - - const skillsRoot = path.join(dir, CODEX_SKILLS_DIR); - const entries = (await fs.readdir(skillsRoot, { withFileTypes: true })).filter((e) => e.isDirectory()); - assert.equal(entries.length, 37, 'exactly 23 supporting and 14 TOH workflow wrappers exist'); - - const NAME_RE = /^[a-z0-9-]{1,64}$/; - for (const entry of entries) { - const raw = await fs.readFile(path.join(skillsRoot, entry.name, 'SKILL.md'), 'utf8'); - const { fm, body } = parseSkillFrontmatter(raw); - - assert.ok(NAME_RE.test(fm.name), `${entry.name}: valid skill name`); - assert.equal(fm.name, entry.name, 'frontmatter name matches directory'); - assert.ok(typeof fm.description === 'string' && fm.description.length > 0, 'description present'); - assert.ok(fm.description.length <= 1024, 'description within Codex limit'); - if (EXPECTED_COMMANDS.includes(entry.name)) { - assert.equal(fm.metadata?.generator, 'toh-framework', 'command generator marker present'); - assert.equal(fm.metadata?.kind, 'command', 'command wrapper kind present'); - } - assert.ok(body.trim().length > 0, 'non-empty instructions'); - - // Every `.toh/...` reference in the body must resolve to a real file. - - const agents = await fs.readFile(path.join(dir, 'AGENTS.md'), 'utf8'); - assert.ok(Buffer.byteLength(agents, 'utf8') <= AGENTS_MAX_BYTES, 'AGENTS.md is below Codex limit'); - const refs = raw.match(/`(\.toh\/[^`]+)`/g) || []; - assert.ok(refs.length > 0, `${entry.name}: references .toh files`); - for (const ref of refs) { - const rel = ref.slice(1, -1); - assert.ok(await fs.pathExists(path.join(dir, rel)), `${entry.name}: resolves ${rel}`); - } - } + const userText = '# My project\n\nKeep this paragraph.\n'; + await fs.writeFile(path.join(dir, 'AGENTS.md'), userText); + + await quickInstall(dir); + const first = await fs.readFile(path.join(dir, 'AGENTS.md'), 'utf8'); + assert.ok(first.startsWith(userText), 'user text must stay at the top'); + + await quickInstall(dir); + const second = await fs.readFile(path.join(dir, 'AGENTS.md'), 'utf8'); + assert.equal(second, first, 'reinstall must be byte-identical'); + assert.equal(second.split('').length - 1, 1); + assert.equal(second.split('').length - 1, 1); + + // The generator reports the block size; Codex truncates at 32 KiB combined, + // so the block must stay under the 24 KiB hard budget. + const bytes = await writeAgentsMd(dir, SRC_DIR, 'en', 'codex'); + assert.ok(bytes > 4000 && bytes < 24 * 1024, `block is ${bytes} bytes`); + + // Native agent tree is identical across reinstalls too. + const tree1 = await snapshotTree(path.join(dir, CODEX_AGENTS_DIR)); + await quickInstall(dir); + const tree2 = await snapshotTree(path.join(dir, CODEX_AGENTS_DIR)); + assert.deepEqual([...tree1.entries()], [...tree2.entries()]); } finally { await fs.remove(dir); } }); -test('skills reference the supporting .toh/skills from command frontmatter', async () => { +test('uninstall --ide codex removes only our native agent files; shared surfaces stay', async () => { const dir = await makeTmpProject(); try { - await quickInstallCodex(dir); - const catalog = await readCommandCatalog(SRC_DIR); - for (const entry of catalog) { - const raw = await fs.readFile( - path.join(dir, CODEX_SKILLS_DIR, entry.skillName, 'SKILL.md'), - 'utf8' - ); - assert.ok(raw.includes(`.toh/commands/${entry.file}`), `${entry.skillName}: references its command file`); - for (const s of entry.skills) { - assert.ok(raw.includes(`.toh/skills/${s}/SKILL.md`), `${entry.skillName}: references skill ${s}`); - assert.ok( - await fs.pathExists(path.join(dir, '.toh', 'skills', s, 'SKILL.md')), - `${entry.skillName}: supporting skill ${s} actually installed` - ); - } - } + await quickInstall(dir); + const edited = path.join(dir, CODEX_AGENTS_DIR, 'ui-builder.toml'); + await fs.appendFile(edited, '\n# my tweak\n'); + const agentsMdBefore = await fs.readFile(path.join(dir, 'AGENTS.md'), 'utf8'); + const configBefore = await fs.readFile(path.join(dir, '.codex', 'config.toml'), 'utf8'); + + const preview = await uninstallCodex(dir, { dryRun: true }); + assert.equal(preview.removedAgents.length, EXPECTED_AGENTS.length - 1); + assert.deepEqual(preview.keptAgents, ['ui-builder']); + assert.ok(await fs.pathExists(path.join(dir, CODEX_AGENTS_DIR, 'dev-builder.toml')), 'dry-run must not delete'); + + const code = await uninstall({ target: dir, ide: 'codex', yes: true }); + assert.equal(code, 0); + assert.equal(await fs.pathExists(path.join(dir, CODEX_AGENTS_DIR, 'dev-builder.toml')), false); + assert.equal(await fs.pathExists(edited), true, 'edited agent must survive'); + assert.equal(await fs.pathExists(path.join(dir, CODEX_MANIFEST_PATH)), false); + assert.equal(await fs.readFile(path.join(dir, 'AGENTS.md'), 'utf8'), agentsMdBefore); + assert.equal(await fs.readFile(path.join(dir, '.codex', 'config.toml'), 'utf8'), configBefore); + assert.ok(await fs.pathExists(path.join(dir, '.toh', 'plan.md'))); + + // A backup copy of what was removed exists. + const backups = await fs.readdir(path.join(dir, '.toh-uninstall-backup')); + assert.ok(backups.some((n) => n.startsWith('codex-agents-'))); } finally { await fs.remove(dir); } }); -test('modified TOH files are preserved because ownership hashes no longer match', async () => { +test('full uninstall removes .codex/agents/ and the manifest, keeps the plan', async () => { const dir = await makeTmpProject(); try { - await quickInstallCodex(dir); - const file = path.join(dir, CODEX_SKILLS_DIR, 'toh-plan', 'SKILL.md'); - const modified = `${await fs.readFile(file, 'utf8')}\nUser customization.\n`; - await fs.writeFile(file, modified); - - await quickInstallCodex(dir); - assert.equal(await fs.readFile(file, 'utf8'), modified, 'reinstall does not overwrite modified skill'); - - const result = await uninstallCodex(dir, { backup: false }); - assert.ok(!result.removedSkills.includes('toh-plan'), 'modified skill is not removed'); - assert.equal(await fs.readFile(file, 'utf8'), modified, 'uninstall keeps modified skill'); + await quickInstall(dir); + const code = await uninstall({ target: dir, yes: true }); + assert.equal(code, 0); + assert.equal(await fs.pathExists(path.join(dir, CODEX_AGENTS_DIR)), false); + assert.equal(await fs.pathExists(path.join(dir, CODEX_MANIFEST_PATH)), false); + assert.equal(await fs.pathExists(path.join(dir, 'AGENTS.md')), false, 'TOH-only AGENTS.md should go'); + assert.ok(await fs.pathExists(path.join(dir, '.toh', 'plan.md')), 'the plan is the user\'s work'); } finally { await fs.remove(dir); } }); -test('AGENTS.md size assertion rejects files above the Codex ceiling', () => { - assert.throws( - () => assertAgentsMdSize('x'.repeat(AGENTS_MAX_BYTES + 1)), - /Codex limit/i - ); -}); - -test('install fails when the final AGENTS.md exceeds the Codex ceiling', async () => { - const dir = await makeTmpProject(); - try { - await fs.writeFile(path.join(dir, 'AGENTS.md'), 'x'.repeat(AGENTS_MAX_BYTES)); - await assert.rejects( - () => quickInstallCodex(dir), - /AGENTS\.md is .*Codex limit/i - ); - } finally { - await fs.remove(dir); - } +test('codex capability profile stays the conservative floor (probe may upgrade it)', () => { + const codex = CAPABILITY_PROFILES.codex; + assert.equal(codex.subagents, 'none'); + assert.equal(codex.parallel, false); + assert.equal(codex.modelRouting, false); }); -test('uninstall dry-run reports owned files without changing the project', async () => { +test('Claude Code agents keep native fields and drop the Codex-only modelIntent', async () => { const dir = await makeTmpProject(); try { - await quickInstallCodex(dir); - const before = await snapshotTree(dir); - const result = await uninstallCodex(dir, { dryRun: true }); - - assert.equal(result.dryRun, true); - assert.equal(result.removedSkills.length, 14); - assert.equal(result.removedAgents.length, 8); - assert.equal(result.agentsMd, 'removed'); - assert.equal(result.config, 'removed'); - assert.deepEqual([...before.keys()].sort(), [...(await snapshotTree(dir)).keys()].sort(), 'dry-run keeps file set'); + await quickInstall(dir, 'claude'); + const raw = await fs.readFile(path.join(dir, '.claude', 'agents', 'ui-builder.md'), 'utf8'); + const { fm } = frontmatterOf(raw); + assert.equal(fm.name, 'ui-builder'); + assert.equal(fm.model, 'sonnet'); + assert.equal(fm.modelIntent, undefined); + assert.equal(await fs.pathExists(path.join(dir, CODEX_AGENTS_DIR)), false, 'claude-only install writes no Codex agents'); } finally { await fs.remove(dir); } }); -test('catalog parsing fails with an actionable error on a broken package', async () => { +test('readAgentCatalog returns [] for a project without .toh/agents', async () => { const dir = await makeTmpProject(); try { - await assert.rejects( - () => installCodexSkills(dir, path.join(dir, 'no-such-src')), - /command source not found/i - ); + assert.deepEqual(await readAgentCatalog(dir), []); } finally { await fs.remove(dir); }