
最近大半年我把能接触到的编码类 AI 工具都用了一遍最后留在我日常工作流里的是 OpenAI 官方的 Codex CLI。它足够快、也足够透明适合在终端里一行一行盯着它改代码。但 Codex 用多了你会发现一个问题它很像一个非常聪明但没有工作纪律的实习生。任务给得足够具体的时候它能惊艳全场任务稍微模糊一点它就开始自由发挥改错文件、漏写测试、甚至顺手把项目结构重排一遍。为了治这个毛病我把 OpenSpec 和 Matt Pocock Skills 跟 Codex 组合在了一起并把整套项目的初始化流程做成了一键脚本。这篇博文会把方案完整拆开三个工具各自解决什么问题、脚本每一段在做什么、实际跑起来会遇到哪些坑、我是怎么排掉的。如果你也在用 Codex 这类编码代理并且受够了“让 AI 改代码像开盲盒”这篇应该正对你胃口。1. 为什么要把 Codex、OpenSpec、Skills 绑在一起三个工具单独拿出来都能用但组合在一起才真正解决了我日常开发的痛点。这一节先讲清楚它们各自是什么、问题出在哪后面看脚本时你才能明白每个步骤存在的意义。1.1 Codex 单独用最难受的三个地方Codex 是 OpenAI 开源的命令行编码代理装好之后在终端里输入codex 描述一个任务它会自己去读仓库、制定改造计划、改代码、跑命令甚至帮你跑测试。它默认读取项目根目录的AGENTS.md作为长期行为约定这本来是个很好的设计但如果仓库里只有一行“be a helpful assistant”那等于没有约定。我用下来的体感最难受的是三点。第一没有流程硬约束。Codex 默认的行为是“接了指令就动手”。改个配置文件这种小活没问题但一个正经功能往往需要先理解需求、写方案、拆任务、逐个实现、最后验证。没有流程约束时它会跳过前面那些步骤直接写代码边写边想经常写到一半发现方向不对再推倒重来。你在旁边盯着还好一旦让它后台跑回来就是一堆没整理的大改动你完全不知道它经历了什么。第二上下文靠猜。Codex 确实会扫描仓库但扫描不等于理解。仓库一大它抓到什么当上下文基本靠模型当时的注意力和实际相关之间经常有差距。差距小的时候是改错注释差距大的时候是改了 A 模块却忽略了 B 模块对它的依赖。我见过它在没有读package.json的情况下自作主张升级了依赖结果整个构建炸掉。第三领域经验不在模型里。模型训练时见过海量代码但“我们团队的测试规范”“这个项目的状态管理约定”这种信息它不可能靠训练学会只能靠项目里的文档喂给它。很多团队不写这些于是每个任务都靠提示词现场口述换个任务又忘了。这三个痛点恰好是另外两样东西能补的位置。1.2 OpenSpec 管的是“流程”不是“代码”OpenSpec 是一套面向 AI 编码代理的规范驱动开发工作流核心是在项目里维护一个openspec/目录里面放四类东西project.md描述项目背景和整体目标specs/放功能需求规格plans/放实施计划tasks/放拆好的可执行任务最后用一个changelog.md记录变更历史。它改变的是 AI 干活的方式。没有 OpenSpec 时你说“帮我加个导出 CSV 的功能”Codex 大概率直接改代码然后回复“完成了”。有 OpenSpec 时它会先读 project.md发现没有对应 spec就会先写一份 spec 描述功能边界、验收标准再拆成 plans 和 tasks然后一项一项做做一项更新一项最后把变更写进 changelog。一次随意的对话变成了可以留痕、可以回滚、可以验收的工程过程。打个比方让 AI 改代码就像给新同事派活。你跟新同事说“把登录页做了”和跟他说“需求文档在这里任务拆成了 8 个你先做前两个每做完一个跑一遍单测再来找我”结果是完全不同的。OpenSpec 就是后面那套“派活方式”。有了它AI 的产出不再是黑盒你随时能打开openspec/tasks/看它干到哪一步了。1.3 Matt Pocock Skills 补的是“手感”Matt Pocock 是 TypeScript/React 社区的知名作者他在 GitHub 上开源了一套 coding agent skills本质上是几十个 markdown 文件每个文件对应一条具体的技能比如怎么写测试、怎么系统化调试、React 状态管理的注意点、TypeScript 类型陷阱等等。每个技能文件里都是一套操作流程不是空话是能直接照着执行的行动指南。这些技能文件不是给 OpenSpec 用的而是给所有支持上下文引用的 AI 编码代理用的。它们和 AGENTS.md 的配合很有意思AGENTS.md 负责告诉 AI“仓库里有什么规矩”技能文件负责告诉它“这类活具体应该怎么干”。实际用法是把技能文件放进仓库的skills/目录然后在 AGENTS.md 里写明“遇到调试任务先读 skills/debugging/debugging.md”让 AI 在需要的时候才去读而不是一股脑全塞进上下文。这三样东西的分工很清楚OpenSpec 管节奏先 spec、再 plan、逐步 implement、最后 verifyMatt Pocock Skills 管手艺让 AI 在具体场景下按成熟流程干活Codex 管执行把前面两者读进去动手改代码。组合确定了新的问题也来了每次开一个新项目我都要手动做一遍很机械的初始化。npm 装 Codex、登录、初始化 openspec/、克隆技能包、手写 AGENTS.md这五六步里漏一步后面就很容易翻车。于是我把这套流程写成了一键脚本核心诉求只有一个一个命令跑完仓库从零变成 AI-ready而且重复执行不会出问题。2. 脚本设计一个命令能搞定什么写代码之前我先列了一份需求清单。后面所有设计决策都是围绕这份清单展开的避免“写到哪里算哪里”。2.1 脚本的第一版需求清单第一条是一条命令完成全部初始化全程不需要人工确认。我受够了那种跑到一半停下来问你“是否继续”的脚本尤其当它跑在第 13 步时你人已经去倒水了。第二条是幂等重复执行不会把已有文件覆盖掉也不会产生重复内容。这一点至关重要因为我最早手动初始化时翻过车第二次跑的时候把第一次的 spec 覆盖了。第三条是环境感知缺依赖就明确报错而不是一路错到最后一堆问题才告诉你“其实是 Node 版本不对”。第四条是失败可诊断每个阶段要有清晰的 ok/warn/error 日志这样出了问题能一眼定位到是环境、登录、还是网络的问题。第五条是尽量不改全局环境不往 .bashrc 里塞东西不偷偷装用户没让装的东西。脚本删掉之后系统要能恢复到原来的样子。这份清单里最重要的就是幂等。所以脚本里凡是会产生文件的步骤都先判断目标存不存在存在就跳过并提示。后面你会在每个函数里看到这种“先检查再动手”的写法不是啰嗦是故意为之。2.2 初始化完成后仓库里应该长什么样脚本跑完后的仓库结构大致如下my-project/ ├── AGENTS.md ├── openspec/ │ ├── project.md │ ├── specs/ │ ├── plans/ │ ├── tasks/ │ └── changelog.md └── skills/ ├── debugging/ │ └── debugging.md ├── testing/ │ ├── writing-tests.md │ └── ... ├── typescript/ └── ...AGENTS.md 是所有 AI 编码代理默认会先读的文件相当于项目对 AI 的入职说明书openspec/ 是流程骨架之后每一个功能迭代都从这里发起skills/ 是技能库按需调用。这样一个仓库交给任何支持 AGENTS.md 的编码代理它都能很快进入工作状态而不需要你重新长篇大论地写提示词。如果仓库里只有其中一样效果会差很多。只有 OpenSpec 没有技能AI 知道要走流程但不一定知道怎么写好测试只有技能没有 OpenSpecAI 知道怎么干活但容易跳过规划直接动手。两者都齐了AGENTS.md 才真正有内容可写。2.3 五个阶段的划分整个脚本我拆成了五个阶段串行执行前一个失败了后面就没有执行的必要这也是后面代码里用set -e的原因。第一步是环境检查确认 node、npm、git 存在且版本满足要求。这一步必须放最前面因为后面每个阶段都要用到它们。第二步是 Codex 安装与登录检查没有 codex 就 npm 全局装一个并且检查是否已有登录凭据没有就提示登录。第三步是 OpenSpec 初始化生成 openspec/ 目录和模板文件。第四步是技能包拉取把 Matt Pocock Skills 仓库的内容复制到 skills/但不保留嵌套的 .git。第五步是生成 AGENTS.md把 OpenSpec 的流程规则和技能包的调用方式写进去整个仓库的 AI 工作约定就建立起来了。2.4 几个关键设计决策为什么用 Bash 而不是 Node 脚本因为这个脚本要跑在“还没有项目”的环境里Bash 是几乎所有环境都自带的东西。用 Node 反而要求用户先装好 Node巧的是 Codex 本身又需要 Node那就出现了先有鸡还是先有蛋的问题。Bash 不需要额外依赖写清楚点就行。为什么技能包要放进仓库而不是全局目录因为放进仓库后每一个 clone 这个仓库的人都会拿到同一份技能技能更新也跟着仓库走不会出现“我这台机器上有技能你那台没有”的割裂。全局目录适合个人仓库适合团队。为什么 AGENTS.md 不把技能全文复制进去而是写成按需引用因为上下文窗口是有限的。所有技能的全文加起来可能上万 token全部塞进去还没开始干活就把预算烧完了还会让模型抓不住重点。只写路径、按需读取是最省 token 也最不容易出问题的做法。3. 脚本核心实现与逐段讲解下面把脚本按阶段拆开讲。我贴的是第一版能跑通的代码不是最终优化版每一段都尽量做了注释。你复制到自己项目里时按实际情况改路径和仓库地址就行。3.1 环境检查与参数解析#!/usr/bin/env bash set -euo pipefail PROJECT_ROOT$(cd ${1:-.} pwd) SKILLS_REPOhttps://github.com/mattpocock/coding-agent-skills.git NEED_LOGIN0 GREEN$\033[32m YELLOW$\033[33m RED$\033[31m NC$\033[0m log_step() { echo -e ${YELLOW}[..]${NC} $*; } log_ok() { echo -e ${GREEN}[ok]${NC} $*; } log_warn() { echo -e ${YELLOW}[warn]${NC} $*; } log_err() { echo -e ${RED}[error]${NC} $* 2; } check_env() { local cmd major for cmd in node npm git; do if ! command -v $cmd /dev/null 21; then log_err 缺少依赖命令: $cmd exit 1 fi done major$(node -p process.versions.node | cut -d. -f1) if [ $major -lt 18 ]; then log_err Codex CLI 需要 Node 18 及以上, 当前是 $(node -v) exit 1 fi log_ok 环境检查通过 $(node -v) }第一行set -euo pipefail是 Bash 脚本的保命三件套。-e让脚本在第一条出错命令处退出避免“带着错误继续跑”的情况-u把未定义变量的引用变成错误防止把空值传进后续命令pipefail则保证管道命令里只要有一段失败整个管道就算失败。这三项一起开脚本的行为才可预测。PROJECT_ROOT$(cd ${1:-.} pwd)这行的意思是把用户传入的第一个参数当目标目录省略时用当前目录然后转成绝对路径。后续所有文件操作都基于这个路径不管用户从哪个目录调用脚本都不会跑偏。check_env里真正有门槛的是 Node 版本检查。Codex CLI 对 Node 版本有硬性要求早版本跑在 Node 16 上会直接报错而且报错信息很含糊不是“版本太低”而是各种莫名其妙的运行时错误。与其让用户自己去查不如脚本开头就把版本卡住。3.2 Codex 的安装与登录态检查setup_codex() { if ! command -v codex /dev/null 21; then log_step 未检测到 codex, 开始全局安装 openai/codex npm install -g openai/codex fi log_ok codex CLI 已就绪 if [ -n ${OPENAI_API_KEY:-} ]; then log_ok 检测到 OPENAI_API_KEY, Codex 可直接使用 elif [ -f $HOME/.codex/auth.json ]; then log_ok 已检测到 $HOME/.codex/auth.json, 视为已登录 else NEED_LOGIN1 log_warn 未检测到登录凭据, 稍后需要手动执行: codex login fi }command -v codex用来判断命令是否存在比which更标准。安装成功后并不代表能直接用还要看有没有登录凭据。我这里做了两层判断设置过OPENAI_API_KEY就算有凭据否则看~/.codex/auth.json存不存在。如果两者都没有脚本不中断但会在最后提醒你手动登录。这里有个经验登录判断不要调用类似codex login status这样的子命令去探测。不同版本 Codex 的子命令和输出格式变过好几次你这个版本没有 status 子命令脚本直接崩在-e上。最稳妥的方式就是判断本地文件最多再判断环境变量。文件路径如果将来变了改一行字符串就行。3.3 OpenSpec 初始化setup_openspec() { if [ -f ${PROJECT_ROOT}/openspec/project.md ]; then log_warn openspec/ 已存在, 跳过初始化 return fi log_step 正在初始化 OpenSpec... (cd ${PROJECT_ROOT} npx --yes openspeclatest init . --force) log_ok openspec/ 初始化完成 }每次进入这个函数先看openspec/project.md在不在。在就说明这个仓库已经初始化过了直接跳过保证幂等不在才开始拉模板。注意我用了npx --yes openspeclatest--yes是跳过 npx 下载包时的确认提示否则第一次运行时会卡在一个 Y/n 提示符等你。--force这个参数是为重复执行准备的但不同版本行为不太一样。如果你用的 OpenSpec 版本不认识它把整个命令简化成npx --yes openspeclatest init .就行。还有一种办法是设置CI1环境变量很多交互式 CLI 检测到 CI 环境会自动放弃提问。我在脚本里没有写死第三种方式因为项目更新太快写死了反而误事。以openspec --help的输出为准就好。3.4 技能包的拉取与落盘fetch_skills() { local tmpdir tmpdir$(mktemp -d) log_step 拉取技能包: ${SKILLS_REPO} git clone --depth 1 $SKILLS_REPO $tmpdir/skills /dev/null 21 mkdir -p ${PROJECT_ROOT}/skills if [ -d $tmpdir/skills/skills ]; then cp -R $tmpdir/skills/skills/. ${PROJECT_ROOT}/skills/ else cp -R $tmpdir/skills/. ${PROJECT_ROOT}/skills/ fi rm -rf $tmpdir log_ok 技能已就位: $(ls ${PROJECT_ROOT}/skills | tr \n ) }这里藏了一个很容易踩的坑技能仓库本身是个 git 仓库如果你直接在项目目录里 clone它会变成一个嵌套的 git 仓库外层 git 会把它当成 gitlink 处理。提交时只记录一个 commit 引用内容不会真正进仓库队友 pull 下来后那个目录基本是空的。所以我先把技能仓库 clone 到一个临时目录再用cp -R把内容复制到项目的 skills/ 目录最后把临时目录整个删掉保证项目里没有多余的.git。git clone --depth 1是为了只拉最新一次提交对这个场景足够了。技能文件本身在仓库里的路径可能有几层嵌套所以代码里判断了一下$tmpdir/skills/skills是否存在存在就复制这一层的内容不存在就复制整个仓库根。这个判断是为了兼容仓库结构后来可能发生的变化复制时源目录结尾的/.是重点它表示“把目录里的所有内容复制过去”而不是新建一个同名目录。3.5 生成 AGENTS.mdgenerate_agents() { if [ -f ${PROJECT_ROOT}/AGENTS.md ]; then log_warn AGENTS.md 已存在, 跳过生成 return fi cat ${PROJECT_ROOT}/AGENTS.md EOF # 项目约定 本仓库使用 OpenSpec 管理工作流程, 领域技能存放在 skills/ 目录。 ## 开发流程 1. 开始任何开发前, 先读 openspec/project.md。 2. 如果 openspec/specs/ 中已有对应 spec, 按 spec 实施。 3. 如果没有, 先按 OpenSpec 流程创建 spec 和 plan, 再进入实现。 4. 每完成一个任务, 更新 openspec/tasks/ 和 openspec/changelog.md。 ## 技能按需引用 - 调试问题前, 先读 skills/debugging/debugging.md。 - 写测试前, 先读 skills/testing/writing-tests.md。 - 改 TypeScript/React 代码前, 先读对应技能文件。 ## 完成标准 - 所有改动都要有测试覆盖。 - 提交前必须运行测试和构建命令。 EOF log_ok AGENTS.md 已生成 }这段用 heredoc 把 AGENTS.md 的内容直接写进文件。注意EOF里的 EOF 带了单引号这是关键不带引号的 heredoc 会把$开头的变量展开脚本里如果有$符号内容就被替换掉了带引号的 heredoc 是纯文本原样写入。写这类模板文件时我几乎总是用带引号的形式。AGENTS.md 的内容是整套方案的粘合剂。它没有把技能文件的内容复制进来而是写成“先读哪个文件再动手”的引用式约定。这样模型在每次任务开始时拿到的是精简的规则说明只有真正遇到调试或写测试这种具体场景才会根据 AGENTS.md 的指引去读对应技能文件。省上下文也减少规则之间的相互干扰。3.6 主流程串联与一条命令跑完main() { check_env setup_codex setup_openspec fetch_skills generate_agents log_ok 初始化完成 if [ $NEED_LOGIN -eq 1 ]; then log_warn 最后一步: 请运行 codex login 完成登录, 再开始使用 fi log_step 下一步: codex \先读 openspec/project.md, 然后按规范完成第一个任务\ } main $主流程就是按顺序调用上面那五个函数。NEED_LOGIN是一个标志位在setup_codex里如果发现没有登录凭据就把它置为 1到了最后统一提醒。这样做的好处是让用户先把所有初始化工作完成再一次性解决登录问题不会被中间打断。脚本全部跑完终端会提示下一步的命令用户直接复制就能开始用 Codex 干活。把这几段代码按顺序拼到一个文件里chmod x之后就是一个完整的一键脚本。我不建议把脚本写成长达几百行的单文件那样后面想调整某个阶段时往往一个改动会牵动全局。拆成函数、每个函数只负责一个阶段是这类初始化脚本最容易维护的结构。4. 实际运行效果与踩坑记录脚本写完是一回事跑起来不出幺蛾子是另一回事。这一节放一个真实运行记录再把我踩过的坑、以及后来加的优化都整理出来。4.1 从空目录到一个 AI-ready 仓库我拿一个真实场景跑了完整流程在~/work/demo里准备写一个命令行小工具执行脚本看输出$ bash init-ai-project.sh ~/work/demo [..] 环境检查通过 (node v20.15.1) [..] 未检测到 codex, 开始全局安装 openai/codex [ok] codex CLI 已就绪 [ok] 检测到 OPENAI_API_KEY, Codex 可直接使用 [..] 正在初始化 OpenSpec... [ok] openspec/ 初始化完成 [..] 拉取技能包: https://github.com/mattpocock/coding-agent-skills.git [ok] 技能已就位: debugging testing typescript react [ok] AGENTS.md 已生成 [ok] 初始化完成 [..] 下一步: codex 先读 openspec/project.md, 然后按规范完成第一个任务跑完之后我直接对 Codex 说codex 先读 openspec/project.md再按 openspec/specs 里的第一个 spec 实施第一个任务跑完测试再更新 changelog。Codex 会先读 AGENTS.md 和 project.md然后按 spec 拆任务、逐个实现、跑验证。最明显的变化是它不再一头扎进代码里而是先告诉我它打算怎么做做完之后 changelog 里留下了记录。整个过程我可以随时打开openspec/tasks/看它干到哪一步而不是等它跑完才面对一个不知所以的 diff。4.2 我实际踩过的坑第一个坑是set -e太严格非致命命令出错也直接退出。我早期版本用codex login status去探测登录态结果某个 Codex 版本改了子命令脚本就死在不该死的地方。后来改成判断本地文件和环境变量这条问题才消失。这类“查证类”命令在脚本里尽量少用能读文件判断就不要靠命令输出。第二个坑是 OpenSpec 初始化时的交互提示。npx --yes只跳过了 npx 自身的确认OpenSpec 自己初始化时还可能问问题。处理办法是加--force或者设置CI1环境变量再不行就用printf y\nn\n | npx ...这种管道方式把答案喂进去。第三版脚本里我干脆把几种兼容方式都写在注释里因为版本更新太快不能只记一种。第三个坑是技能仓库嵌套 .git这也是我前面特意设计临时目录复制流程的原因。第一次直接把技能仓库 clone 进项目提交时只看到一个 160000 权限的 gitlink 条目队友 pull 下来后整个 skills 目录是空的。这种错误不是当场爆出来的而是过了好几个 commit 之后才被发现排查成本很高。第四个坑是 AGENTS.md 塞满技能全文。有一版我图省事把十几个技能文件的全文拼进 AGENTS.md结果 Codex 每次对话都要把这几万 token 的规则背一遍响应慢、费 token而且规则多了之后模型反而抓不住重点。改成引用式之后这个问题立刻消失。4.3 脚本后续的小优化第一版脚本能用但跑起来有点傻不管技能包有没有更新每次都重新 clone 一遍。后来我改成先用git ls-remote拿远程仓库的最新 commit跟本地记录的 commit 对比没有变化就跳过下载有变化才重新拉取。这一改重复执行时间从几十秒降到了几秒。我又加了一个--minimal参数。临时写个 demo 项目时没必要把全部技能都拉下来只保留 debugging 和 testing 两个核心技能就够了。这个参数在参数解析阶段用一个全局变量控制后面fetch_skills里根据它决定复制哪些目录。再后来我把脚本放进了团队仓库的scripts/目录。新成员 clone 项目后自己跑一遍得到的仓库结构和 AI 工作约定都一致省去了在群里一遍遍解释“你要先装这个、再跑那个”的功夫。团队里每个人初始化出来的仓库长得一模一样后面协作时遇到的怪问题也少了。4.4 问题速查表症状常见原因处理办法codex命令找不到npm 全局 bin 不在 PATH用 nvm 或 Node 官方安装包重新装安装 CLI 报 EACCESnpm 全局目录权限不足用 nvm 管理 Node避免用 sudoOpenSpec 初始化卡住CLI 在等交互式输入加--force或设置CI1项目里出现了空的子目录直接把技能仓库 clone 进项目用临时目录克隆再cp -RCodex 每次对话都很慢AGENTS.md 里塞了全文技能改成按需引用的路径写法脚本第二次运行报错阶段没有幂等判断文件存在就跳过用-f判断这套脚本我实际用了三周左右最大的感受是AI 编码代理不会因为一份 AGENTS.md 就变成神但有了 OpenSpec 的流程约束和技能包的领域指引它的下限明显提高了。以前我最怕的是 agent 跑完我都不知道它改了什么现在打开 openspec/tasks 就能看到它按什么顺序做了什么万一跑偏也能很快判断是 spec 没写清楚还是实现环节出了问题。如果你也在折腾 Codex我建议别一上来就贪多先跑通最小组合——AGENTS.md 加 OpenSpec再慢慢把技能包加进去。