
Cs Onboard
- 1.3k installs
- 1.1k repo stars
- Updated August 3, 2026
- liuzhengdongfortest/codestable
cs-onboard is an agent skill for 把新仓库或有零散文档的仓库接入 codestable 体系,两条路径自动判断:空仓库从零搭骨架,已有文档走审计 + 迁移映射。触发:用户说"在这个项目里用 codestable"、"搭 codestable 结构"、"初始化 codestable"、"迁移到 codestable"。.
About
The cs-onboard skill is designed for 把新仓库或有零散文档的仓库接入 CodeStable 体系,两条路径自动判断:空仓库从零搭骨架,已有文档走审计 + 迁移映射。触发:用户说"在这个项目里用 CodeStable"、"搭 CodeStable 结构"、"初始化 CodeStable"、"迁移到 CodeStable"。. Glob 全仓库 .md(排除 node_modules/ .git/):根目录 DESIGN.md / ARCHITECTURE.md / SPEC.md / README.md;docs/ doc/ design/ spec/ wiki/;现有 .codestable/ 下文件 4. Invoke when the user asks about cs onboard or related SKILL.md workflows.
- 项目名 / 简介(用于填 ARCHITECTURE.md 占位).
- attention.md 只建最小骨架;用户已经给出的项目硬约束才写入,不凭空代填.
- .codestable/{requirements,roadmap,features,issues,compound}/.gitkeep.
- .codestable/attention.md(最小骨架模板见同目录 reference.md).
- .codestable/architecture/ARCHITECTURE.md(占位模板见同目录 reference.md).
Cs Onboard by the numbers
- 1,280 all-time installs (skills.sh)
- +14 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #315 of 1,880 Design & UI/UX skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
cs-onboard capabilities & compatibility
- Capabilities
- 项目名 / 简介(用于填 architecture.md 占位) · attention.md 只建最小骨架;用户已经给出的项目硬约束才写入,不凭空代填 · .codestable/{requirements,roadmap,features,issue · .codestable/attention.md(最小骨架模板见同目录 reference.md
- Use cases
- frontend
What cs-onboard says it does
把新仓库或有零散文档的仓库接入 CodeStable 体系,两条路径自动判断:空仓库从零搭骨架,已有文档走审计 + 迁移映射。触发:用户说"在这个项目里用 CodeStable"、"搭 CodeStable 结构"、"初始化 CodeStable"、"迁移到 CodeStable"。
把新仓库或有零散文档的仓库接入 CodeStable 体系,两条路径自动判断:空仓库从零搭骨架,已有文档走审计 + 迁移映射。触发:用户说"在这个项目里用 CodeStable"、"搭 CodeStable 结构"、"初始化 CodeStable"、"迁移到 CodeStable"。
npx skills add https://github.com/liuzhengdongfortest/codestable --skill cs-onboardAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.3k |
|---|---|
| repo stars | ★ 1.1k |
| Security audit | 3 / 3 scanners passed |
| Last updated | August 3, 2026 |
| Repository | liuzhengdongfortest/codestable ↗ |
How do I 把新仓库或有零散文档的仓库接入 codestable 体系,两条路径自动判断:空仓库从零搭骨架,已有文档走审计 + 迁移映射。触发:用户说"在这个项目里用 codestable"、"搭 codestable 结构"、"初始化 codestable"、"迁移到 codestable"。?
把新仓库或有零散文档的仓库接入 CodeStable 体系,两条路径自动判断:空仓库从零搭骨架,已有文档走审计 + 迁移映射。触发:用户说"在这个项目里用 CodeStable"、"搭 CodeStable 结构"、"初始化 CodeStable"、"迁移到 CodeStable"。.
Who is it for?
Developers using cs onboard workflows documented in SKILL.md.
Skip if: Skip when the task falls outside cs-onboard scope or needs a different stack.
When should I use this skill?
User asks about cs onboard or related SKILL.md workflows.
What you get
Completed cs-onboard workflow with documented commands, files, and expected deliverables.
- `.codestable/architecture/ARCHITECTURE.md` skeleton
- `.codestable/attention.md` minimum template
By the numbers
- ARCHITECTURE.md template defines 5 numbered architecture sections
- attention.md template includes 7 operational topic headings
Files
cs-onboard
把仓库接入 CodeStable 工作流体系——白纸或已有零散文档的都行。本技能只做两件事:搭骨架、归旧档。骨架搭好后子工作流(feature / issue / compound 等)即可直接运行。
---
两条路径
| 路径 | 适用 | 产出 |
|---|---|---|
| 空仓库 | 仓库内无 spec 类文档,也没有 .codestable/ | 完整骨架 + 必要骨架文件 |
| 迁移 | 仓库内有零散文档 / docs/ / 部分 .codestable/ 结构 | 审计报告 + 迁移映射方案(用户逐条确认)+ 落盘 |
启动后先扫一次自动判断,不要让用户选——TA 大概率不知道项目里现有哪些文档。扫描结果模糊(如只有 README)就明说判断依据并问用户。
---
标准骨架(目标状态)
共享路径与命名约定的权威版本是项目里的 .codestable/reference/shared-conventions.md——本技能从技能包复制过去。下面只列 onboard 创建 / 检查的骨架文件。.codestable/
├── attention.md CodeStable 技能启动必读的项目注意事项
├── requirements/ 需求聚合根(空目录 .gitkeep)
├── architecture/
│ └── ARCHITECTURE.md 架构总入口(首次创建为占位模板)
├── roadmap/ 规划层聚合根
├── features/ feature 聚合根
├── issues/ issue 聚合根
├── compound/ 沉淀类统一目录(learning / trick / decision / explore)
├── tools/ 跨工作流共享脚本(onboard 释放)
│ ├── search-yaml.py
│ └── validate-yaml.py
└── reference/ 跨子技能共享参考(onboard 释放)
├── shared-conventions.md
├── tools.md
└── maintainer-notes.md---
启动检查
先检查一次现状:
1. 检查 `.codestable/`:不存在 → 空仓库候选;存在但不完整 → 迁移(部分补齐) 2. 旧 CodeStable兼容 CodeStable 经过多次改名,从 easysdd 到 codestable 再到 .codestable,如果遇到旧版的codestable目录,提示用户:
检测到旧版codestable。建议直接 git mv easysdd .codestable,结构 / frontmatter 完全兼容,rename 后即用。要我执行吗?同意 → git mv easysdd .codestable,按迁移路径走(这时只需补齐可能缺失的 attention.md、tools/ 和 reference/)。想保留旧目录 → 告诉他子技能只读 .codestable/,旧目录不会被读;按空仓库路径走新骨架
3. Glob 全仓库 `.md`(排除 node_modules/ .git/):根目录 DESIGN.md / ARCHITECTURE.md / SPEC.md / README.md;docs/ doc/ design/ spec/ wiki/;现有 .codestable/ 下文件 4. 检查 `.codestable/attention.md`:缺失则列为骨架待补齐项 5. 汇报扫描结论:找到的相关文档(列路径)+ 走哪条路径 + 判断依据 + 不确定项
---
空仓库路径
步骤 1:和用户确认范围
- 项目名 / 简介(用于填
ARCHITECTURE.md占位) - attention.md 只建最小骨架;用户已经给出的项目硬约束才写入,不凭空代填
步骤 2:创建目录骨架
按下面顺序执行,不等用户逐步确认——骨架是整体一次性的:
.codestable/{requirements,roadmap,features,issues,compound}/.gitkeep.codestable/attention.md(最小骨架模板见同目录reference.md).codestable/architecture/ARCHITECTURE.md(占位模板见同目录reference.md).codestable/tools/(用cp -rf/Copy-Item -Recurse -Force整目录拷贝技能包cs-onboard/tools/,不要 Read 再 Write).codestable/reference/(同上)
落盘用 shell 整目录覆盖,不要 Read 再 Write——这两个目录是机器共享资产,Read+Write 会截断大文件、改缩进、吃空行,还慢费 token。具体命令见迁移路径步骤 4。
步骤 3:attention.md 提醒
attention.md 已创建但默认只有空骨架。汇报时提醒用户:有编译前置、测试命令、目录禁区、凭证规则这类"每次 CodeStable 技能启动都必须知道"的信息,后续用 cs-note 一条条追加。
步骤 4:验收汇报
列建了哪些文件:
CodeStable 骨架已就绪。现在可以:开始新功能cs-feat/ 报告问题cs-issue/ 沉淀知识cs-learn
---
迁移路径
步骤 1:生成审计报告
| 现有文件 | 推测内容类型 | 建议归入 CodeStable | 置信度 |
|---|---|---|---|
docs/DESIGN.md | 项目架构 | .codestable/architecture/ARCHITECTURE.md | 高 |
docs/feature-auth.md | 功能设计稿 | .codestable/features/YYYY-MM-DD-auth/auth-design.md | 中 |
SPEC.md | 功能需求? | 需用户确认 | 低 |
置信度:高 = 语义明确匹配;中 = 可推断有歧义;低 = 不明确或映射多个位置都合理。
步骤 2:逐条对齐
中 / 低置信度的用 AskUserQuestion 问:
- 中:给推断理由,问"按这个方式归位?"
- 低:描述文件内容,给 2-3 个候选位置 + "跳过"
高置信度不逐条问但要在汇报里列,给用户复审机会——逐条问会让节奏失控。
步骤 3:处理已部分存在的 .codestable/
- 命名不符规范(
YYYY-MM-DD-{slug}格式)但有内容 → 提示用户问是否重命名 - 空占位(
.gitkeep/ 空.md)→ 直接补齐不问
步骤 4:补齐缺失骨架
对照标准骨架补齐用户确认后仍缺失的目录 / 文件。已有内容不覆盖。
`.codestable/tools/` 和 `.codestable/reference/` 一律用技能包新版本覆盖——这两个目录是技能包维护的共享资产,权威源在 cs-onboard/tools/ 和 cs-onboard/reference/,项目里的只是落盘副本。技能包升级后再跑 onboard 的目的之一就是刷新副本,留旧版本会让子技能按过时口径工作。
覆盖前在汇报列出被覆盖文件让用户知道;用户明确说"我改过 tools/xxx.py 请保留"才例外保留并标红。这是迁移路径唯一强制覆盖的动作,其他已有文件遵守"不经确认不动"。
落盘命令:
# macOS / Linux
cp -rf <技能包路径>/cs-onboard/tools/. .codestable/tools/
cp -rf <技能包路径>/cs-onboard/reference/. .codestable/reference/
# Windows PowerShell
Copy-Item -Recurse -Force <技能包路径>\cs-onboard\tools\* .codestable\tools\
Copy-Item -Recurse -Force <技能包路径>\cs-onboard\reference\* .codestable\reference\不要:Read+Write 手工搬(截断 / 改缩进)、一个个 cp(多步骤多出错)、先比 diff(规则就是无条件覆盖)。
技能包路径一般是 skill 安装目录(~/.claude/skills/cs-onboard/ 或插件目录)。不确定先 ls 定位。拷完 ls .codestable/tools/ .codestable/reference/ 验证。
步骤 5:处理不迁移的文件
用户选"跳过"的文件:不移动 / 不删除 / 不重命名,汇报标"保留原位(未纳入 CodeStable)"。绝不允许未经确认就动——onboard 只允许 AI 整理不允许替用户做删除决定。
步骤 6:attention.md 提醒(同空仓库路径步骤 3)
步骤 7:验收汇报
列:迁移文件清单(from → to)、新建骨架、未迁移文件(保留原位)、下一步建议。
---
骨架文件模板
ARCHITECTURE.md 占位模板和 attention.md 最小模板见同目录 reference.md。
---
退出条件
- [ ]
.codestable/八个子目录都存在 - [ ]
.codestable/attention.md已建 - [ ]
.codestable/tools/和.codestable/reference/已从技能包复制 - [ ]
.codestable/architecture/ARCHITECTURE.md已建 - [ ] 迁移路径:每条映射都有明确处理结果(迁移 / 保留原位)
- [ ] 迁移路径:没有未经确认就移动的文件
- [ ] 验收汇报已给出
---
容易踩的坑
- 未经确认就移动 / 删除已有文件——迁移核心原则是用户拍板
- 替用户填 attention.md 实质内容——必须项目 owner 来定,AI 只提供模板
- 重新引入 `AGENTS.md` / `CLAUDE.md` 兼容路径——CodeStable 的启动注意事项入口固定为
.codestable/attention.md - 建完骨架立刻开始 feature/issue——onboard 是"搭环境"不是"开始干活"
- 低置信度直接执行——低 = 必须问
- `.codestable/tools/` 和 `.codestable/reference/` 走"不覆盖"保守策略——这两个必须用技能包新版本覆盖,否则升级后用户停留在过时口径
- 用 Read + Write 手工搬——必须
cp -rf/Copy-Item -Recurse -Force整目录覆盖 - Glob 时忘记排除 `node_modules/` `.git/`——会让扫描结果充斥噪声
---
相关文档
.codestable/reference/system-overview.md— CodeStable 体系总览.codestable/reference/shared-conventions.md— 目录结构和共享口径的权威版本.codestable/attention.md— CodeStable 技能启动必读的项目注意事项.codestable/architecture/ARCHITECTURE.md— 架构总入口骨架
onboard 参考模板
本文件提供 cs-onboard 使用的骨架模板。
1. .codestable/architecture/ARCHITECTURE.md 占位模板
# {项目名} 架构总入口
> 状态:骨架(待填充)
> 创建日期:YYYY-MM-DD
## 1. 项目简介
## 2. 核心概念 / 术语表
## 3. 子系统 / 模块索引
## 4. 关键架构决定
## 5. 已知约束 / 硬边界2. .codestable/attention.md 最小模板
attention.md 是 CodeStable 技能启动必读的项目注意事项入口。onboard 创建最小骨架,不替项目 owner 填实质内容;后续短规则由 cs-note 追加。
# Attention
本文件是 CodeStable 技能启动必读的项目注意事项入口。所有 CodeStable 子技能开始工作前必须读取它。
## 项目碎片知识
<!-- cs-note managed: 用 cs-note 维护,新条目按下面分节追加 -->
### 编译与构建
### 运行与本地起服务
### 测试
### 命令与脚本陷阱
### 路径与目录约定
### 环境变量与凭证
### 其他代码维度速查
写代码前先确认每个维度的档位。没明说的走默认,偏离默认的地方要标出来让用户确认。
这份文档是 CodeStable 子技能共享的口径,被 design / fastforward / issue-fix 等阶段引用。项目内的权威副本在 .codestable/reference/code-dimensions.md,由 cs-onboard 从技能包释放。
---
核心四维(每次都要定)
健壮性 Robustness —— 错误处理的严苛程度
- L1 快跑:happy path 跑通就行,异常直接崩、让它炸。适合一次性脚本、探索代码。
- L2 够用:捕获预期错误(文件不存在、网络超时),非预期错误往上抛。适合内部工具。
- L3 严防:所有外部输入验证、所有失败路径都有明确处理、关键操作幂等可重试。适合对外接口、生产系统。
结构 Structure —— 代码组织的颗粒度
- inline:全写在一起,十几行搞定的那种。
- functions:按职责拆函数,同一个文件内。
- modules:拆多个文件/模块,有明确的导入关系。
- layers:分层架构(如 handler / service / repository),有依赖方向约束。
性能 Performance —— 对开销的关注度
- careless:怎么方便怎么写,O(n²) 也无所谓。
- reasonable:避开明显的坑(循环里查 DB、重复计算),但不刻意优化。
- budgeted:有明确的性能预算(延迟、内存、QPS),按预算设计数据结构和算法。
- extreme:榨性能,要 profiling、要基准测试、可以牺牲可读性。
可读性 Readability —— 写给谁看
- self:自己当下看得懂就行,命名可以随意。
- team:队友半年后还能快速上手,命名规范、关键处有注释。
- public:外部开发者能无背景读懂,公共 API 要有文档、示例。
- teaching:代码本身就是教材,每一步意图清晰、刻意展示模式。
---
场景维度(相关时才定)
可演进性 Evolvability —— 预期会怎么变
- frozen:接口锁死,不许改(如已发布的库 API)。
- stable:偶尔变,变动要走流程、要兼容。
- active:当前在迭代,接口随业务调整。
- experimental:随时推倒重来,不考虑向后兼容。
可观测性 Observability —— 运行时能看到多少
- opaque:黑盒,出了问题靠猜。
- logged:关键路径有日志,能事后翻查。
- traced:有链路追踪,跨服务能串起来。
- instrumented:指标齐全(metrics / traces / logs 三件套),可接告警。
可测试性 Testability —— 测试覆盖的深度
- untested:没测试。
- testable:结构支持测试(依赖可注入、副作用可隔离),但还没写。
- tested:有单元/集成测试覆盖主要路径。
- verified:核心逻辑有测试 + 关键不变量有断言/属性测试/形式化验证。
安全性 Security —— 信任边界
- trusted:全在可信环境内,不设防。
- validated:外部输入做校验和清洗。
- sandboxed:权限最小化、危险操作隔离(容器、subprocess 限权)。
- hardened:按对抗性环境设计,防注入/防越权/防侧信道,有威胁模型。
---
特殊维度(只在涉及时提)
- Concurrency 并发:single-threaded / thread-safe / lock-free / distributed
- Determinism 确定性:nondeterministic / reproducible / deterministic
- Compatibility 兼容性:current-only / backward-compatible / cross-version
- Idempotency 幂等性:non-idempotent / idempotent / exactly-once
---
常用默认组合
| 场景 | 组合 |
|---|---|
| 聊天里问的随手代码 | L1 + inline + careless + self + experimental |
| 项目内部工具 | L2 + functions + reasonable + team + active + logged + testable |
| 对外发布的库/服务 | L3 + modules + budgeted + public + stable + traced + tested + validated |
没明说就按场景走默认。动手前列出关键档位,偏离默认的地方明确标出来让用户确认。
---
怎么用这份文档
- design / fastforward 起草时:AI 先按场景猜默认组合,把判断出的"可能偏离默认"的维度列出来问用户;用户没明确说的维度按默认走。只记偏离项,默认档位不抄。
- implement / fix 写代码时:翻一眼当前 feature 或 issue 记录的维度档位,按档位写。比如记了
健壮性=L3就不要偷工省掉输入校验;记了可读性=public就得补示例和文档。 - acceptance / review 时:把维度档位当成验收标准的一部分——档位说 L3 但代码里外部输入没校验,就是不达标。
CodeStable 维护者说明
本文件由 cs-onboard 复制到项目的 .codestable/reference/maintainer-notes.md。维护 CodeStable 技能家族时需要反复查阅、但不适合放在各子技能正文里的说明。
---
1. 断点恢复
AI 对话随时可能中断(token 超限、网络断开、用户换设备)。各阶段发现自己不是从零开始时,必须优先检查已有产物的完成度,从上次停下的地方继续:
- brainstorm:如
{slug}-brainstorm.md已有部分内容,读取后问用户"上次聊到 X,要接着聊还是推翻重来?" - design:如
{slug}-design.md已有部分节,逐节检查完成度,补齐缺失节,不重写已完成节 - implement:
{slug}-checklist.yaml中已done的步骤不重做,从第一个pending步骤开始 - acceptance:如
{slug}-acceptance.md已有部分节,检查哪些节已填写(有实质 checklist 勾选),从下一个未完成节继续 - issue-analyze:如
{slug}-analysis.md已存在,检查 5 节是否都有内容,缺失的补做,已有的不重写 - issue-fix:如代码已改但
{slug}-fix-note.md不存在,直接进入验证 + 写 fix-note 环节
恢复时先向用户简短汇报:"检测到上次工作到 X 阶段,我从 Y 继续"。
---
2. 扩展点
新增子工作流
新工作流定型后,在 cs-onboard/reference/system-overview.md 的"技能分成四部分"和"场景路由"表里加一段索引,并登记新的目录位置。
跨阶段新约束
如果发现某条规则适用于所有阶段(例如所有 spec doc 都必须补某个字段),优先写进共享 reference(shared-conventions.md 或 system-overview.md),不要只改一个子技能。
新模板 / 新产物类型
如果引入新的 spec 产物(例如风险评估表、回滚预案),先在 shared-conventions.md 登记路径,再在对应阶段技能里引用。
共享术语表
如果 CodeStable 自己形成了稳定共享术语,应优先沉淀成共享 reference,而不是散落在多个子技能里重复定义。
跨工作流状态一览
目前查看"项目当前有几个 feature 在进行中、几个 issue 未关闭"仍需要手动查询。未来如要补 status.py 或 .codestable/STATUS.md,先在 shared-conventions.md 登记方向,再实现。
---
3. 维护规则
- 每次扩展都要同步更新
system-overview.md索引和相关子技能 - 不允许只在某个子技能里加东西而不在
system-overview.md登记 - 共享说明优先放
.codestable/reference/,不要散落在各子技能里
requirement 文档示例
下面这份示例取自 CodeStable 自己的能力(修 bug 时的探索分析流),用来展示一份好的 requirement doc 的语气、结构、颗粒度。新项目做 onboard 时随包落盘,之后写自己的 requirement 可以直接照着改。
---
写作要点速查
- 标题直接平铺说这能力是什么,不玩比喻、不起花哨名字。
- 用户故事顶在最前面,每条要能想象出一个具体处境。
- 为什么需要 / 怎么解决 各一段短的,不上课、不展开。
- 边界用列表,至少写一条"它不管什么"。
- 不写实现细节——"通过 X 接口调用 Y 服务"这种挪到 architecture doc。
- frontmatter 的 `pitch` 要去技术化、一句话、读者没上下文也能看懂,以后当宣传词用。
---
示例正文
````markdown --- doc_type: requirement slug: issue-flow pitch: 修 bug 时先让 AI 探索和分析,再动手改 status: current last_reviewed: 2026-04-21 implemented_by:
- arch-cs-issue
tags: [debug, ai-assist] ---
修 bug 时先探索和分析
用户故事
- 作为一个刚接手别人代码的人,我希望把报错直接丢给 AI,它告诉我根因在哪,而不是自己翻三个文件摸调用链。
- 作为一个被线上问题打断的开发,我希望 AI 帮我收窄嫌疑范围,而不是自己从 git log 一条条比对。
- 作为一个只记得"点那个按钮就白屏"的人,我希望 AI 反过来问我几个问题把现场补清楚,而不是让我自己想该给它什么信息。
为什么需要
修 bug 的难点不在改代码,在定位。线索通常零碎(一段报错、一个截图、一句口述),从这点信息摸到真正的根因,往往要先自己耗掉半小时。对不熟的模块、对新接手的人,这段成本更高。
怎么解决
先让 AI 读现场——日志、代码、git 历史——交叉验证之后讲清楚"哪里坏了、为什么坏、改动会影响什么"。人确认过再动手改,改完验证。
边界
- 不主动扫 bug,得你先感知到异常给它入口。
- 线索实在不够时它会反问你补现场,而不是瞎猜。
- 不处理"还没想清楚要做什么"——那是需求 / 设计的事。
````
---
反面样例(不要这样写)
这几种写法 AI 很容易默认写出来,都是典型"翻车":
语气像在上课
本能力旨在通过智能化的探索与分析机制,为开发者提供高效的缺陷定位解决方案……
改成"修 bug 的难点不在改代码,在定位"。
标题玩比喻
让 AI 当你修 bug 时的第一个读者
改成"修 bug 时先探索和分析"。
用户故事太抽象
作为用户,我希望系统好用。
删掉。用户故事必须能想象出具体处境。
把实现细节塞进来
通过调用代码检索服务和 Git 日志分析模块,对报错日志进行上下文推理……
这是 architecture doc 的事,从 requirement 里删掉。
CodeStable 共享口径
由 cs-onboard 复制到项目的 .codestable/reference/shared-conventions.md。所有 CodeStable 子技能用项目相对路径 .codestable/reference/shared-conventions.md 引用本文件——跨子技能共享但不适合堆在单个技能里的规范的唯一权威版本。
skill 本身不共享文件系统(每个 skill 是独立安装单元),共享口径不能放在某个 skill 内部被别的 skill 引用。放在"工作项目"里对所有 skill 都可达。
---
0. 目录结构与路径命名
onboard 完成后骨架(cs-onboard 负责搭建):
.codestable/
├── attention.md CodeStable 技能启动必读的项目注意事项
├── requirements/ 能力愿景层("用户需要什么、系统提供什么能力来满足",过去/现在/未来)
│ ├── VISION.md 中心索引(按 status 分组,每条带 pitch 一句话)
│ └── {slug}.md 一个能力一份,扁平(cs-req 产出)
├── architecture/ 架构中心目录("用什么结构实现",只记现状)
│ ├── ARCHITECTURE.md 总入口(索引 + 关键架构决定)
│ └── {type}-{slug}.md 子系统 / 模块 doc(cs-arch 产出)
├── roadmap/ 规划层("接下来怎么做这块大需求 + 模块怎么切 + 接口怎么定")
│ └── {slug}/ 一个大需求一个子目录(cs-roadmap 产出)
│ ├── {slug}-roadmap.md 主文档:背景 / 范围 / 模块拆分 / 接口契约 / 子 feature 清单 / 排期
│ ├── {slug}-items.yaml 机器可读子 feature 清单,acceptance 回写状态
│ └── drafts/ 可选
├── features/ feature spec 聚合根
│ └── YYYY-MM-DD-{slug}/ 每个 feature 一个目录
│ ├── {slug}-brainstorm.md (可选,case 2 时产出)
│ ├── {slug}-design.md (标准流程)
│ ├── {slug}-checklist.yaml (标准流程)
│ ├── {slug}-acceptance.md (标准流程)
│ └── {slug}-ff-note.md (fastforward 通道唯一产物,与上面四份互斥)
├── issues/ issue spec 聚合根
│ └── YYYY-MM-DD-{slug}/
│ ├── {slug}-report.md
│ ├── {slug}-analysis.md (根因不显然才有)
│ └── {slug}-fix-note.md
├── refactors/ refactor spec 聚合根
│ └── YYYY-MM-DD-{slug}/
│ ├── {slug}-scan.md
│ ├── {slug}-refactor-design.md
│ ├── {slug}-checklist.yaml
│ └── {slug}-apply-notes.md
├── compound/ 沉淀类文档统一目录
│ └── YYYY-MM-DD-{doc_type}-{slug}.md
│ doc_type ∈ {learning, trick, decision, explore}
├── brainstorm/ brainstorm 阶段 spike 实验代码区(cs-brainstorm 临时产出)
│ └── {slug}/ 一次 spike 一个子目录,文件名随意
│ 验完不强制清理,结论回写到对应 brainstorm note
├── tools/ 跨工作流共享脚本(onboard 从技能包释放)
└── reference/ 共享参考文档(onboard 从技能包释放)命名规则
- 需求文档:
requirements/{slug}.md(能力愿景,不带日期前缀,扁平不分组);中心索引requirements/VISION.md - roadmap:
roadmap/{slug}/(不带日期前缀,平铺不嵌套) - feature / issue / refactor 目录:带日期前缀
YYYY-MM-DD-{slug} - 沉淀类:
compound/YYYY-MM-DD-{doc_type}-{slug}.md,日期用归档当天 - 架构 doc:
architecture/{type}-{slug}.md(长效,不带日期前缀);总入口固定ARCHITECTURE.md - 项目注意事项入口固定为
.codestable/attention.md,所有 CodeStable 子技能启动前必须读取;不再兼容AGENTS.md/CLAUDE.md等外部入口
架构 doc 分组规则(同类聚合)
architecture/ 下用文件名第一段作 type 标记:ui-chat.md 和 ui-events.md 同 ui 类。所有架构 doc 必须 `{type}-{slug}.md`——只有一份的也要带合理 type 段(如 cli-entry.md),否则未来同类出现时聚合不了。
触发:某 type 在 architecture/ 根目录达到 ≥6 份时(即新加第 6 份那次),把这一类全部收进同名子目录。
收入后:去掉 type 前缀。ui-chat.md → ui/chat.md。
只升不降:删到 ≤5 份也不折回平铺。
触发时谁负责:cs-arch 的 backfill / update 模式在 Phase 6 落盘前主动检查并搬迁;命中阈值时这次操作要把"本次新加 / 改的 + 已有同类全部"一起搬,并同步改 ARCHITECTURE.md 链接(搬迁本身要在 Phase 5 给用户 review,不偷偷做)。check 模式不主动搬迁,但发现 ≥6 仍平铺时在报告末尾列为观察项。
改目录结构
改 cs-onboard/reference/shared-conventions.md 模板,新项目 onboard 时带上新版本;已有项目手动同步 .codestable/reference/shared-conventions.md。
---
1. 共享元数据口径
feature spec:brainstorm / design / acceptance 共用 doc_type / feature / status / summary / tags。子技能只补特有字段。status:brainstorm = confirmed(落盘即确认无 draft);design = draft / approved;acceptance 见对应技能。
issue spec:report / analysis / fix-note 共用 doc_type / issue / status / tags。severity / root_cause_type / path 由对应阶段按需补。
归档类(compound):
- learning / trick / decision / explore 四类统一写入 `.codestable/compound/`
- 每个文档 frontmatter 顶部带
doc_type(learning / trick / decision / explore)作跨子技能归属判定 - 文件名
YYYY-MM-DD-{doc_type}-{slug}.md——日期打头便于ls排序,type 段在中间便于 grep - 各子技能在
doc_type之外保留专属 frontmatter(learning 的track/ trick 的type/ decision 的category/ explore 的type) - 各子技能只认自己的
doc_type不读写别家 status等通用字段语义和本文件保持一致
外部读者文档(guidedoc / libdoc):frontmatter 由各自子技能定义。无特殊说明:draft = 待 review,current = 当前有效,outdated = 代码已变更待同步。
写作约束:子技能提字段时优先写"额外字段"或"阶段状态变化",不重复展开整套通用字段。
---
2. {slug}-checklist.yaml 生命周期
- 是 feature 工作流的唯一执行清单
- 由
cs-feat-design在 design 确认通过后一次生成steps+checks cs-feat-ff不生成 checklist(也不写 design / acceptance),是跳过 spec 流程直接写代码的超轻量通道;唯一留下的痕迹是动手后回写的{slug}-ff-note.md(轻量回顾,参与 scoped-commit、可被 cs-arch / cs-req backfill 检索到)
steps 的粒度是 编排-计算分离维度的切片策略——按"先编排骨架、后计算节点、最后持久化与测试"写(最简 Workflow 先行 → 逐个节点填充),不下沉到 file:line / 函数级。具体改哪个文件由 implement 阶段决定。
design 的职责:
- 提取
steps(4-8 步,每步独立可验证退出信号):后端节奏 = 编排骨架 → 计算节点逐个填 → 接通持久化 → 测试覆盖;前端 = 静态结构 → 交互逻辑 → 状态接入 → 联调收尾 - 提取
checks:第 1 节"明确不做"→ 范围守护;第 2.1 接口 → 名词契约;第 2.2 主流程 + 流程级约束 → 编排骨架;第 2.3 挂载点 → 挂载点;第 3 节场景清单 → 验收场景
implement 的职责:
- 按
steps顺序执行,每步完成把 statuspending→done - 实现到具体文件级时需要拆分某步、或发现微重构是其前置(参考第 7 节反射检查)→ 跟用户对齐后追加 / 拆分 steps,不偷偷做
- 不改写
checks
acceptance 的职责:只更新 checks[].status(pending → passed / failed),不重写 steps。
写作约束:子技能描述 checklist 时只补本阶段读 / 写哪一部分,不重新定义生命周期。
---
2.5 roadmap ↔ feature 衔接协议
.codestable/roadmap/{slug}/{slug}-items.yaml 是规划层和 feature 执行层的唯一接口。三个技能共同读写它——是 skill 都读写项目共享产物,不算耦合。
items.yaml 状态机:
planned → in-progress (cs-feat-design 启动 feature 时改)
in-progress → done (cs-feat-accept 验收完成时改)
planned → dropped (cs-roadmap update 模式,用户决定不做时改)done / dropped 是终态。需要回退重做的新加一条 slug 略改的条目,不改终态。
cs-roadmap 的职责:生成和维护 roadmap 主文档 + items.yaml;把 planned 改 dropped(用户放弃时);不改 in-progress / done(feature 技能负责)。
cs-feat-design 的职责(从 roadmap 起头时):
1. design.md frontmatter 加 roadmap: {roadmap-slug} + roadmap_item: {子 feature slug} 2. items.yaml 对应条目 status: in-progress + feature: YYYY-MM-DD-{slug} 3. 校验 yaml
直接起 feature(非 roadmap 来)两字段留空,不触发 roadmap 写。
cs-feat-accept 的职责:
1. 读 design frontmatter roadmap / roadmap_item 2. 空 → 跳过 3. 有值 → items.yaml 对应条目 status: done;同步主文档子 feature 清单显示状态;校验 yaml
回写是实际写文件的动作,验收报告要明确记录回写结果。
最小闭环标记:items.yaml 每份只有一条 minimal_loop: true,标记"做完后系统能端到端跑通最窄路径"。design 启动 minimal_loop 条目时优先级最高。
---
3. 阶段收尾推荐
feature-acceptance 收尾按顺序判断:
1. cs-learn:沉淀经验 2. cs-decide:长期约束 / 选型 3. cs-guide:开发者 / 用户指南 4. cs-libdoc:公开 API 参考 5. scoped-commit
issue-fix 收尾按顺序判断:
1. cs-learn:坑点 2. cs-decide:暴露的长期约束 3. scoped-commit
feature-ff 收尾按顺序判断(比标准 acceptance 短,没有 architecture / req 回写动作):
1. cs-learn:动手过程暴露的坑 2. cs-decide:动手过程拍板的长期约束 3. scoped-commit
统一规则:一律一句话提示;用户说"不用"立即跳过;不强制;上游主动提示,下游承接执行。
---
4. 收尾提交(scoped-commit)
acceptance / issue-fix 走完后把本次产物提交为一个 commit:
- 范围:本次工作改到的代码 + 相关 spec 文档 + 本次实际更新过的架构 doc + 本次实际更新过的 roadmap items.yaml / 主文档
- 不该进:和本次工作无关的顺手修改;属于"下次另起 feature / issue"的扩大范围
- 提交前确认:用户没明确同意不要
git commit - commit message:一句话说清"做了什么",不贴 spec 目录路径
子技能只描述本阶段特有提交范围,通用规则看这里。
---
5. 归档检索规则
feature-design / issue-analyze / issue-fix 动手前到 .codestable/compound/ 搜已有沉淀:
- 总是先搜
architecture/和compound/ - 在
compound/用doc_type过滤(learning / trick / decision / explore) - 搜到的结果只作参考输入,不盲目套用——可能已
outdated或不适合当前上下文 - 搜到和当前方向冲突的 decision → 必须正面回应"为什么仍然这么做"或调整方向
子技能只补本阶段查询命令。完整搜索语法看 .codestable/reference/tools.md。
---
6. 归档类子技能共享守护规则
cs-learn / cs-trick / cs-decide / cs-explore 共享下面这组规则。子技能正文只写特有反模式,通用看这里:
1. 只增不删——已归档除非被明确取代(status=superseded)否则不删;理由丢失成本极高 2. 宁缺毋滥——用户说不出理由的节直接省略,不要 AI 编造 3. 不替用户写实质内容——AI 负责起草结构和串联语言,实质结论必须来自用户或可追溯的代码证据 4. attention.md 检查——写完后若沉淀暴露出"每次启动都该知道"的一两行硬约束,提示用户用 cs-note 追加到 .codestable/attention.md;不要直接改外部 AI 入口 5. 起草前先查重叠——动手写前用 search-yaml.py --query 查语义相近的旧文档。命中就把候选列给用户在三条路径里选:
- 更新已有(默认优先):沿用原文件名和原创建日期,不新建;frontmatter 补
updated: YYYY-MM-DD;超出小修在文末加"YYYY-MM-DD 更新"简述 - supersede:旧文档保留原文,
status: superseded+superseded-by: {新文件名},正文顶部加**[已取代]** 见 {新 slug};新文档 frontmatter 带supersedes: {旧文件名} - 确实是不同主题:新建,文末"相关文档"列出已有那条说明区别
6. 识别用户意图是"改已有"还是"记新的"——用户说"改 / 更新 / 修订 / 补充 {某条}"、明确指向某条旧文档、或话题高度重合时默认走"更新已有",不要闷头新建。分不清就问。
各子技能只认自己的 doc_type,不读写别家产物。
---
7. 写代码时的反射检查
cs-feat-impl 和 cs-issue-fix 共用。AI 默认会往"大函数 / 大文件 / god class / 处处特殊分支"漂,这一节把漂移截在发生那一刻。
不是阈值,是触发器——硬数字会诱发为拆而拆把自然聚合的代码切碎。每条都是"遇到 X 情况就停下来问自己"。
| 触发场景 | 停下来问自己 |
|---|---|
| 要往一个已经很长的文件追加代码时 | 文件承担几件事?新加的是已有职责延伸还是第 N+1 件事?是第 N+1 就默认新建文件 |
| 要给已经很多方法的类加方法时 | 新方法是核心职责的自然扩展,还是把类推向"什么都能干"? |
| 写的函数已超过一屏时 | 函数在做几件事?几件事就拆 |
要加 if (特殊情况) { 特殊处理 } 分支时 | 抽象维度选错了?正确做法可能是把特殊路径和通用路径分成不同函数 / 策略 / 类 |
| 要 copy-paste 一段代码时 | 能抽成共用还是只字面相似?能抽就抽 |
| 要给函数加第 4+ 个参数时 | 函数做的事是不是太多了?参数列表是 API 恶化的早期信号 |
| 要新写"万能工具类 / helper"时 | 真没归属还是只是想不起来放哪儿就先堆 util? |
停下来之后:反射检查只把问题提出来,结论用户定。停下来想清楚的动作(拆 / 新建 / 重命名 / 抽共用)会让改动超出现有 steps 范围 → 跟用户对齐再决定(纳入当前推进 / 记顺手发现留后续)。
不许偷偷拆完继续写,也不许忽略信号硬冲。默认动作是停、问、再继续。
CodeStable 体系总览
本文档介绍 CodeStable 工作流家族整体——有哪些子技能、各管什么场景、产物怎么组织。无论是 AI 在运行时读到这个文件,还是人打开来看,都能对整个体系有个完整印象。
AI 辅助开发里,有几类场景会反复出现——加新功能、修 bug、遇到值得沉淀的经验、做技术选型、摸新模块的代码、接入新仓库。每种场景如果每次从零处理,都会出各自的典型问题:AI 给功能起的术语跟老代码冲突、bug 改完没人记得当时怎么诊断的、上周刚踩过的坑下周又踩一遍。
CodeStable 把这几类场景各配一套子技能,产物放进统一的目录结构、带统一的 YAML frontmatter,互相之间可以检索引用。
技能分成四部分
根入口——开放式诉求 / 不知道走哪个时的统一入口:
cs— 介绍体系全貌 + 把诉求路由到正确的 cs-* 子技能。本技能不做事,只做分诊和提示
做事——从一段模糊想法走到上线的功能、或者从一份错误报告走到修好的 bug:
cs-feat— 新功能,design → implement → acceptance(想法还模糊时先走讨论层cs-brainstorm做分诊,不属于 feature 流程内部)cs-issue— 修 bug,report → analyze → fixcs-refactor— 代码优化(行为不变、结构/性能/可读性变),scan → design → apply
两类都不直接让 AI 写代码,而是先产出 spec(功能方案 / 问题分析),用户 review 后再动手,代码和 doc 一起交付。针对的是术语冲突、范围失控、改完不留存档这三种 AI 默认会出的问题。
沉淀——把做事过程产生的知识存下来,下次遇到同类问题直接复用:
cs-learn— 回顾"做 X 时踩了 Y 这个坑"cs-trick— 处方"以后做 X 就这样做"cs-decide— 规定"全项目今后都按 X 来"cs-explore— 存档"调查了 X 问题,看到代码里是这样的"cs-note— 把一两行启动必读的项目注意事项追加到.codestable/attention.md
讨论层——想法还模糊时的统一入口,不直接产出设计或代码:
cs-brainstorm— 和用户对话做分诊:case 1(已经够清楚,直接 feature-design)、case 2(小需求,在 feature 里继续讨论并落{slug}-brainstorm.md)、case 3(大需求,移交给 roadmap)
辅助——围着前几类转的周边工具:
cs-onboard— 把新仓库接入 CodeStable 目录结构cs-req— 起草或刷新.codestable/requirements/下的需求文档——系统的能力愿景层,覆盖过去/现在/未来cs-arch— 架构相关一站式:起草新架构文档 / 刷新已有文档 / 做架构体检(含 design 自洽 / design↔代码一致 / architecture 目录多份文档间一致)。architecture 只记现状cs-roadmap— 把一块装不进单个 feature 的大需求拆成带依赖和状态的子 feature 清单,作为后续多次 feature 流程的种子和排期依据;独立于需求 / 架构档案cs-guide— 写给外部读者的开发者指南 / 用户指南cs-libdoc— 为库的公开 API 逐条目生成参考文档
场景路由
仓库里还没有 .codestable/ 目录,先用 cs-onboard 搭骨架。
| 场景 | 子技能 |
|---|---|
| 想法还模糊 / "有个想法没想清楚" / "先聊聊" | cs-brainstorm(分诊后路由到 design / feature-brainstorm 落盘 / roadmap) |
| 新功能 / 新能力 | cs-feat |
| BUG / 异常 / 文档错误 | cs-issue |
| 代码优化 / 重构 / 重写(行为不变) | cs-refactor |
| 摸代码、提问调研 | cs-explore |
| 补 / 更新需求文档 | cs-req |
| 补 / 更新 / 检查架构文档 | cs-arch |
| 大需求拆解 / 排期规划 | cs-roadmap |
| 技术选型 / 约束 / 规约 | cs-decide |
| 踩坑回顾、经验总结 | cs-learn |
| 可复用的编程模式、库用法 | cs-trick |
| 开发者指南 / 用户指南 | cs-guide |
| 库 API 参考 | cs-libdoc |
完整的操作手册、退出条件、和其他工作流的关系,各子技能里讲。
沉淀类四个子技能如何区分
learning / trick / decision / explore 都是存档文档类型,区别在记录内容的性质:
- 回顾某次做 X 时发现了 Y ——
cs-learn(产出doc_type: learning) - 以后做 X 就这样做的处方 ——
cs-trick(产出doc_type: trick) - 全项目今后都得遵守的规定 ——
cs-decide(产出doc_type: decision) - 调查了一个问题,留份证据 ——
cs-explore(产出doc_type: explore)
四者共用 .codestable/compound/ 目录,靠 frontmatter 的 doc_type 字段和文件名中间的类型段(YYYY-MM-DD-{doc_type}-{slug}.md)区分。每个子技能只认自己的 doc_type,不读写别家产物——"A 和 B 有什么不同"这种判断由本节负责,子技能里不再重复。
愿景档案 vs 结构档案 vs 规划档案 vs 单次动作
四类文档各管一段时间尺度,不要混:
- 愿景档案(requirements)——描述"用户需要什么、系统提供什么能力来满足"。
status区分三个时间深度:draft(未来愿景)、current(现在的能力)、outdated(过去的痕迹)。draft req 可独立于实现存在——先把愿景定下来,后续 roadmap 排期和 design 实现才有稳定对齐基准 - 结构档案(architecture)——描述"系统现在用什么结构实现"。只记现状,默认在 feature-acceptance 时跟着代码同步;必要时由 cs-arch 主动刷新。不写"未来会加什么层"
- 规划档案(roadmap)——描述"接下来打算怎么分步实现"。独立于愿景和结构档案,改动不牵连 requirements / architecture。所有条目 done / dropped 后 roadmap 进入
completed状态,作为历史档案留存 - 单次动作(feature / issue / refactor)——本次要做的一件具体事情的 spec。动作走完后,相关沉淀提炼进愿景档案、结构档案和沉淀类文档
用户说"我想要一个 X 系统"这种大需求,先走 roadmap 拆成若干子 feature,再一条一条走 feature 流程。直接起 feature 会变成巨型 design 塞不下、拆了又没有追踪抓手。
feature 和 issue 的阶段不可跳
feature 走 brainstorm(可选) → design → implement → acceptance,issue 走 report → analyze → fix。每个阶段有退出条件,上一个没满足,下一个不开始。
AI 最常见的问题是一口气铺几百行代码才让人看——等发现问题已经很难中止。阶段间的人工 checkpoint 就是为了早一步中止。每个 checkpoint 具体检查什么,对应子技能里讲。
例外两种:issue 根因一眼确定时走快速通道,跳过 analyze 直接 fix;feature 范围小时走 cs-feat-ff,写完 spec 直接进实现。
进一步参考
.codestable/reference/shared-conventions.md— 目录结构、YAML frontmatter 口径、{slug}-checklist.yaml生命周期、收尾 commit 约定、归档类共享规则.codestable/reference/tools.md—search-yaml.py/validate-yaml.py用法.codestable/reference/maintainer-notes.md— 断点恢复、新增子工作流的登记
目录结构(requirements/、architecture/、roadmap/、features/、issues/、compound/、tools/、reference/)的权威定义在 shared-conventions.md。要改目录先改那里——方法是改 cs-onboard/reference/shared-conventions.md 这个模板,新项目 onboard 时会带上新版本。
相关
.codestable/attention.md— CodeStable 技能启动必读的项目注意事项.codestable/architecture/ARCHITECTURE.md— 项目架构总入口
CodeStable 工具用法参考
本文件由 cs-onboard 复制到项目的 .codestable/reference/tools.md,所有 CodeStable 子技能用项目相对路径 .codestable/reference/tools.md 引用。
.codestable/tools/ 下共享脚本的完整用法参考。子技能里只写本技能特有的 1-2 行典型查询;完整语法和示例看这里。
---
1. search-yaml.py
通用 YAML frontmatter 搜索工具。从项目根目录运行,无需安装额外依赖(PyYAML 可选,有则用,无则内建 fallback parser)。
基本语法
python .codestable/tools/search-yaml.py --dir {目录} [--filter key=value]... [--query "全文关键词"] [--sort-by FIELD [--order asc|desc]] [--full] [--json]filter 语法
key=value:字段精确匹配(大小写不敏感)key~=value:字符串字段子串匹配;列表字段元素包含匹配key=a|b|c/key~=a|b|c:同一字段多个候选值,候选之间是 OR;在 PowerShell / Bash 中请给整个 filter 加引号,例如--filter "doc_type=decision|explore|learning"
排序语法
--sort-by FIELD:按 frontmatter 字段排序(典型字段:last_reviewed、date、updated_at)--order desc|asc:desc默认,新的在前;asc老的在前(查"谁最久没更新"用这个)- 字段缺失 / 值为空的文档一律排到最后,不干扰前排结论
常用命令
沉淀类文档统一在 .codestable/compound/,用 doc_type 字段区分四个子技能的产物,内部还有各自的细分字段:
# 按 doc_type 筛选
python .codestable/tools/search-yaml.py --dir .codestable/compound --filter doc_type=learning
python .codestable/tools/search-yaml.py --dir .codestable/compound --filter "doc_type=decision|explore|learning" --filter status=active
python .codestable/tools/search-yaml.py --dir .codestable/compound --filter doc_type=decision --filter status=active
python .codestable/tools/search-yaml.py --dir .codestable/compound --filter doc_type=trick --filter status=active
python .codestable/tools/search-yaml.py --dir .codestable/compound --filter doc_type=explore --filter status=active
# doc_type + 子技能内部细分字段
python .codestable/tools/search-yaml.py --dir .codestable/compound --filter doc_type=learning --filter track=pitfall
python .codestable/tools/search-yaml.py --dir .codestable/compound --filter doc_type=decision --filter category=constraint
python .codestable/tools/search-yaml.py --dir .codestable/compound --filter doc_type=trick --filter type=pattern
python .codestable/tools/search-yaml.py --dir .codestable/compound --filter doc_type=explore --filter type=question
# 按 tag(列表元素包含匹配)
python .codestable/tools/search-yaml.py --dir .codestable/compound --filter tags~=prisma
# 全文搜索
python .codestable/tools/search-yaml.py --dir .codestable/compound --query "shadow database"
# 按领域/框架/语言筛选
python .codestable/tools/search-yaml.py --dir .codestable/compound --filter doc_type=decision --filter area=frontend
python .codestable/tools/search-yaml.py --dir .codestable/compound --filter doc_type=trick --filter framework~=vue
python .codestable/tools/search-yaml.py --dir .codestable/compound --filter doc_type=trick --filter language=typescript
# 搜索 feature 方案 doc
python .codestable/tools/search-yaml.py --dir .codestable/features --filter doc_type=feature-design --filter status=approved
# 输出控制
python .codestable/tools/search-yaml.py --dir .codestable/compound --filter doc_type=decision --filter status=active --full
python .codestable/tools/search-yaml.py --dir .codestable/compound --filter tags~=llm --json
# 按时间排序
python .codestable/tools/search-yaml.py --dir .codestable/compound --sort-by date --order desc # 最近归档的在前
python .codestable/tools/search-yaml.py --dir .codestable/library-docs --sort-by last_reviewed --order asc # 最久没 review 的在前(找陈旧文档)
python .codestable/tools/search-yaml.py --dir .codestable/guides --filter status=current --sort-by last_reviewed --order asc典型使用场景
| 场景 | 命令建议 |
|---|---|
| feature-design 开始前查已有归档 | 搜 .codestable/compound 目录,按 --query "{关键词}" 全文搜;要分类看就加 `--filter "doc_type=learning\ |
| issue-analyze 根因分析前查历史 | 搜 .codestable/compound --filter doc_type=learning --filter track=pitfall、再搜 --filter doc_type=trick --filter type=library,按相关组件/框架过滤 |
| 归档落盘后查重叠 | 搜 .codestable/compound --query "{关键词}" --json,看有无语义重叠 |
| 新人了解项目规约 | --dir .codestable/compound --filter doc_type=decision --filter status=active |
| 按技术栈浏览技巧 | --dir .codestable/compound --filter doc_type=trick --filter language={语言} --filter status=active |
| 找最久没 review 的库文档 / 指南 | --dir {目录} --filter status=current --sort-by last_reviewed --order asc |
| 看最近沉淀了哪些经验 | --dir .codestable/compound --filter doc_type=learning --sort-by date --order desc |
---
2. validate-yaml.py
YAML 语法校验工具。用于验证 frontmatter 语法和必填字段。
# 校验单个文件的 YAML 语法
python .codestable/tools/validate-yaml.py --file {文件路径} --yaml-only
# 校验必填字段
python .codestable/tools/validate-yaml.py --file {文件路径} --require doc_type --require status
# 批量校验目录下所有文件
python .codestable/tools/validate-yaml.py --dir {目录} --require doc_type --require status#!/usr/bin/env python3
"""
search-yaml.py — Generic YAML-frontmatter search tool for markdown document directories.
Works on any directory of .md files that use YAML frontmatter (--- ... ---).
Designed for AI agent use: fast, structured output, no required external dependencies.
Filter syntax (--filter flag, repeatable, AND logic):
key=value Exact match on a scalar field (case-insensitive)
key=a|b Exact match against any candidate value (OR)
key~=value Substring match on a string field, or element-in for list fields
key~=a|b Substring/list match against any candidate value (OR)
Usage examples:
# Search .codestable/compound (learning / trick / decision / explore docs share this dir)
python .codestable/tools/search-yaml.py --dir .codestable/compound --filter doc_type=learning --filter track=pitfall
python .codestable/tools/search-yaml.py --dir .codestable/compound --filter "doc_type=decision|explore|learning"
python .codestable/tools/search-yaml.py --dir .codestable/compound --filter doc_type=trick --filter tags~=prisma
python .codestable/tools/search-yaml.py --dir .codestable/compound --filter doc_type=decision --filter status=active --full
# Full-text search in body + frontmatter values
python .codestable/tools/search-yaml.py --dir .codestable/compound --query "shadow database"
# JSON output for AI agent consumption
python .codestable/tools/search-yaml.py --dir .codestable/compound --filter doc_type=learning --filter track=knowledge --json
# Sort by a frontmatter date field (works on any ISO-8601 date string, YAML date, or sortable value)
python .codestable/tools/search-yaml.py --dir .codestable/library-docs --sort-by last_reviewed --order asc # oldest first (stalest)
python .codestable/tools/search-yaml.py --dir .codestable/compound --sort-by date --order desc # newest first
# Works on any yaml-frontmatter markdown directory
python .codestable/tools/search-yaml.py --dir docs/decisions --filter status=accepted
python .codestable/tools/search-yaml.py --dir content/posts --filter tags~=python --query "asyncio"
"""
import argparse
import json
import sys
from pathlib import Path
try:
import yaml # type: ignore
_HAS_PYYAML = True
except ImportError:
_HAS_PYYAML = False
# ---------------------------------------------------------------------------
# Frontmatter parsing (PyYAML used when available, builtin fallback otherwise)
# ---------------------------------------------------------------------------
def _parse_yaml_scalar(val: str):
val = val.strip()
if val.startswith("[") and val.endswith("]"):
inner = val[1:-1]
return [item.strip().strip("'\"") for item in inner.split(",") if item.strip()]
lower = val.lower()
if lower in ("true", "yes"):
return True
if lower in ("false", "no"):
return False
if lower in ("null", "~", ""):
return None
return val
def parse_frontmatter(text: str) -> tuple[dict, str]:
"""
Split a markdown document into (frontmatter_dict, body_text).
Returns ({}, full_text) when no frontmatter is present.
"""
if not text.startswith("---"):
return {}, text
end = text.find("\n---", 3)
if end == -1:
return {}, text
fm_text = text[3:end].strip()
body = text[end + 4:].strip()
if _HAS_PYYAML:
try:
meta = yaml.safe_load(fm_text)
return (meta or {}), body
except yaml.YAMLError:
# Malformed frontmatter — fall through to the lenient builtin parser
# so partial / hand-written frontmatter still produces best-effort results.
pass
# Minimal fallback: handles scalar values and inline lists
meta: dict = {}
for line in fm_text.splitlines():
if not line.strip() or line.startswith("#") or ":" not in line:
continue
key, _, raw = line.partition(":")
meta[key.strip()] = _parse_yaml_scalar(raw)
return meta, body
# ---------------------------------------------------------------------------
# Document loading
# ---------------------------------------------------------------------------
def load_documents(directory: Path) -> list[dict]:
docs = []
for md_file in sorted(directory.rglob("*.md")):
try:
text = md_file.read_text(encoding="utf-8")
except OSError as exc:
print(f"[warn] Cannot read {md_file.name}: {exc}", file=sys.stderr)
continue
meta, body = parse_frontmatter(text)
docs.append({
"file": str(md_file.relative_to(directory)),
"path": str(md_file),
"meta": meta,
"body": body,
})
return docs
# ---------------------------------------------------------------------------
# Filter parsing and evaluation
# ---------------------------------------------------------------------------
def _split_filter_values(value: str) -> list[str]:
values = [part.strip() for part in value.split("|")]
return [part for part in values if part] or [value.strip()]
class Filter:
"""Parsed representation of a single --filter expression."""
def __init__(self, raw: str):
if "~=" in raw:
key, _, value = raw.partition("~=")
self.key = key.strip()
self.value = value.strip()
self.values = _split_filter_values(self.value)
self.operator = "contains"
elif "=" in raw:
key, _, value = raw.partition("=")
self.key = key.strip()
self.value = value.strip()
self.values = _split_filter_values(self.value)
self.operator = "exact"
else:
raise argparse.ArgumentTypeError(
f"Invalid filter expression {raw!r}. "
"Use 'key=value' for exact match or 'key~=value' for substring/list-contains match. "
"Use pipes for OR values, e.g. 'doc_type=decision|explore|learning'."
)
def matches(self, meta: dict) -> bool:
field_val = meta.get(self.key)
if field_val is None:
return False
if self.operator == "exact":
return any(str(field_val).lower() == value.lower() for value in self.values)
# contains: substring for strings, element-in for lists
if isinstance(field_val, list):
return any(
value.lower() == str(item).lower()
for value in self.values
for item in field_val
)
return any(value.lower() in str(field_val).lower() for value in self.values)
def __repr__(self):
op = "~=" if self.operator == "contains" else "="
return f"Filter({self.key}{op}{self.value})"
def parse_filter(raw: str) -> Filter:
"""argparse type converter for --filter."""
return Filter(raw)
_MISSING = object()
def _sort_key(doc: dict, field: str):
"""
Sort key for --sort-by. Docs missing the field sort to the end regardless
of --order. Dates (datetime.date / datetime.datetime) and strings are both
normalized to their string form — ISO 8601 date strings sort the same
lexicographically as YAML-parsed date objects' isoformat().
"""
val = doc["meta"].get(field, _MISSING)
if val is _MISSING or val is None:
return (1, "")
try:
return (0, val.isoformat()) # datetime.date / datetime.datetime
except AttributeError:
return (0, str(val))
def doc_matches(doc: dict, filters: list[Filter], query: str | None) -> bool:
meta = doc["meta"]
for f in filters:
if not f.matches(meta):
return False
if query:
needle = query.lower()
haystack = doc["body"].lower() + " " + " ".join(str(v) for v in meta.values()).lower()
if needle not in haystack:
return False
return True
# ---------------------------------------------------------------------------
# Output formatting
# ---------------------------------------------------------------------------
def _meta_summary(meta: dict) -> str:
"""One-line summary of frontmatter fields, skipping slug/date for brevity."""
skip = {"slug"}
parts = []
for k, v in meta.items():
if k in skip:
continue
if isinstance(v, list):
parts.append(f"{k}=[{', '.join(str(i) for i in v)}]")
else:
parts.append(f"{k}={v}")
return " ".join(parts)
def format_summary(doc: dict) -> str:
return f"### {doc['file']}\n{_meta_summary(doc['meta'])}"
def format_full(doc: dict) -> str:
return format_summary(doc) + "\n\n" + doc["body"]
def print_text(results: list[dict], full: bool) -> None:
print(f"Found {len(results)} document(s).\n")
sep = "\n" + "─" * 60 + "\n"
chunks = [format_full(d) if full else format_summary(d) for d in results]
print(sep.join(chunks))
def print_json(results: list[dict], full: bool) -> None:
output = []
for doc in results:
body = doc["body"]
if not full and len(body) > 400:
body = body[:400] + "…"
output.append({"file": doc["file"], "meta": doc["meta"], "body": body})
print(json.dumps(output, ensure_ascii=False, indent=2))
# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------
def _build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Generic YAML-frontmatter search across a directory of markdown files.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
parser.add_argument("--dir", metavar="DIR", required=True,
help="Directory of .md files to search.")
parser.add_argument("--filter", "-f", metavar="EXPR", dest="filters",
type=parse_filter, action="append", default=[],
help="Frontmatter filter expression. Repeatable (AND logic). "
"key=value for exact match; key~=value for substring (strings) or element-in (lists). "
"Use pipes for OR values, e.g. key=a|b.")
parser.add_argument("--query", "-q", metavar="TEXT",
help="Full-text search in document body and frontmatter values.")
parser.add_argument("--full", action="store_true",
help="Print full document body instead of just the frontmatter summary.")
parser.add_argument("--json", dest="as_json", action="store_true",
help="Output results as a JSON array.")
parser.add_argument("--sort-by", metavar="FIELD", dest="sort_by",
help="Sort results by a frontmatter field (e.g. last_reviewed, date, updated_at). "
"ISO-8601 date strings and YAML-parsed dates both sort correctly. "
"Docs missing the field are pushed to the end.")
parser.add_argument("--order", choices=("asc", "desc"), default="desc",
help="Sort order when --sort-by is set. Default: desc (newest first).")
return parser
def _resolve_directory(dir_arg: str) -> Path:
directory = Path(dir_arg)
if not directory.exists():
print(f"[error] Directory not found: {directory}", file=sys.stderr)
sys.exit(1)
if not directory.is_dir():
print(f"[error] Not a directory: {directory}", file=sys.stderr)
sys.exit(1)
return directory
def _sort_results(results: list[dict], sort_by: str, order: str) -> list[dict]:
def has_field(d: dict) -> bool:
return sort_by in d["meta"] and d["meta"][sort_by] is not None
present = [d for d in results if has_field(d)]
missing = [d for d in results if not has_field(d)]
present.sort(key=lambda d: _sort_key(d, sort_by), reverse=(order == "desc"))
return present + missing
def main() -> None:
args = _build_parser().parse_args()
directory = _resolve_directory(args.dir)
docs = load_documents(directory)
if not docs:
print(f"No .md files found in {directory}")
return
results = [d for d in docs if doc_matches(d, args.filters, args.query)]
if not results:
print("No matching documents found.")
return
if args.sort_by:
results = _sort_results(results, args.sort_by, args.order)
if args.as_json:
print_json(results, full=args.full)
else:
print_text(results, full=args.full)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
validate-yaml.py — Validate YAML frontmatter syntax in markdown files.
Scans markdown files for YAML frontmatter (--- ... ---) and checks:
1. Frontmatter block is properly delimited (opening and closing ---)
2. YAML syntax is valid (parseable without errors)
3. (Optional) Required fields are present (--require flag)
Designed for AI agent use: structured output, exit code reflects pass/fail,
no required external dependencies (falls back to builtin parser if PyYAML unavailable).
Usage examples:
# Validate all .md files under codestable/features
python codestable/tools/validate-yaml.py --dir codestable/features
# Validate a single file
python codestable/tools/validate-yaml.py --file codestable/features/2026-04-11-auth/auth-design.md
# Check that required fields exist in frontmatter
python codestable/tools/validate-yaml.py --dir codestable/features --require doc_type --require status
# JSON output for programmatic consumption
python codestable/tools/validate-yaml.py --dir docs/api --json
# Validate the libdoc manifest
python codestable/tools/validate-yaml.py --file docs/api/manifest.yaml --yaml-only
"""
import argparse
import json
import sys
from pathlib import Path
# Force UTF-8 stdout/stderr on Windows where default codepage (e.g. GBK / cp936)
# can't encode the ✓ / ✗ icons used in text output. Safe no-op on POSIX.
# Streams that aren't a real TextIOWrapper (e.g. captured by pytest, redirected
# through some IDEs) raise io.UnsupportedOperation — a ValueError + OSError
# subclass — and we just leave the original encoding in place.
for _stream in (sys.stdout, sys.stderr):
if hasattr(_stream, "reconfigure"):
try:
_stream.reconfigure(encoding="utf-8")
except (OSError, ValueError):
pass
# ---------------------------------------------------------------------------
# YAML parsing
# ---------------------------------------------------------------------------
_HAS_PYYAML = False
try:
import yaml # type: ignore
_HAS_PYYAML = True
except ImportError:
pass
def _builtin_parse_yaml(text: str) -> dict:
"""Minimal YAML parser for flat key-value frontmatter (no nested structures)."""
result: dict = {}
for line in text.splitlines():
stripped = line.strip()
if not stripped or stripped.startswith("#") or ":" not in stripped:
continue
key, _, raw = stripped.partition(":")
val = raw.strip()
# Inline list
if val.startswith("[") and val.endswith("]"):
inner = val[1:-1]
result[key.strip()] = [
item.strip().strip("'\"") for item in inner.split(",") if item.strip()
]
else:
result[key.strip()] = val.strip("'\"") if val else ""
return result
def parse_yaml_text(text: str) -> tuple[dict | None, str | None]:
"""
Parse a YAML string. Returns (parsed_dict, None) on success,
or (None, error_message) on failure.
"""
if _HAS_PYYAML:
try:
result = yaml.safe_load(text)
if result is None:
return {}, None
if not isinstance(result, dict):
return None, f"Expected a mapping, got {type(result).__name__}"
return result, None
except yaml.YAMLError as exc:
return None, str(exc)
else:
# Builtin fallback — can only detect gross syntax issues
try:
result = _builtin_parse_yaml(text)
return result, None
except Exception as exc:
return None, str(exc)
# ---------------------------------------------------------------------------
# Frontmatter extraction
# ---------------------------------------------------------------------------
def extract_frontmatter(text: str) -> tuple[str | None, str | None]:
"""
Extract YAML frontmatter from a markdown file.
Returns (frontmatter_text, None) on success,
or (None, error_message) if frontmatter is missing or malformed.
"""
if not text.startswith("---"):
return None, "No opening '---' delimiter found"
end = text.find("\n---", 3)
if end == -1:
return None, "No closing '---' delimiter found (frontmatter block not terminated)"
fm_text = text[3:end].strip()
if not fm_text:
return None, "Frontmatter block is empty"
return fm_text, None
# ---------------------------------------------------------------------------
# Validation logic
# ---------------------------------------------------------------------------
class ValidationResult:
def __init__(self, file_path: str):
self.file = file_path
self.errors: list[str] = []
self.warnings: list[str] = []
self.fields: list[str] = [] # fields found in frontmatter
@property
def ok(self) -> bool:
return len(self.errors) == 0
def to_dict(self) -> dict:
d: dict = {"file": self.file, "status": "pass" if self.ok else "fail"}
if self.errors:
d["errors"] = self.errors
if self.warnings:
d["warnings"] = self.warnings
if self.fields:
d["fields"] = self.fields
return d
def _check_required(parsed: dict | None, required_fields: list[str] | None, result: ValidationResult) -> None:
if not required_fields:
return
for field in required_fields:
if field not in (parsed or {}):
result.errors.append(f"Missing required field: '{field}'")
def _warn_if_builtin(result: ValidationResult) -> None:
if not _HAS_PYYAML:
result.warnings.append(
"PyYAML not installed — using builtin fallback parser "
"(may miss some syntax errors). Install with: pip install pyyaml"
)
def _validate_file(
file_path: Path,
required_fields: list[str] | None,
base_dir: Path | None,
mode: str, # "markdown" | "yaml"
) -> ValidationResult:
display_path = str(file_path.relative_to(base_dir)) if base_dir else str(file_path)
result = ValidationResult(display_path)
try:
text = file_path.read_text(encoding="utf-8")
except OSError as exc:
result.errors.append(f"Cannot read file: {exc}")
return result
if mode == "markdown":
yaml_text, extract_err = extract_frontmatter(text)
if extract_err:
result.errors.append(extract_err)
return result
else:
yaml_text = text
parsed, parse_err = parse_yaml_text(yaml_text)
if parse_err:
result.errors.append(f"YAML syntax error: {parse_err}")
return result
result.fields = list(parsed.keys()) if parsed else []
_check_required(parsed, required_fields, result)
_warn_if_builtin(result)
return result
def validate_markdown_file(file_path, required_fields=None, base_dir=None):
"""Validate YAML frontmatter in a single markdown file."""
return _validate_file(file_path, required_fields, base_dir, "markdown")
def validate_yaml_file(file_path, required_fields=None, base_dir=None):
"""Validate a pure YAML file (not markdown with frontmatter)."""
return _validate_file(file_path, required_fields, base_dir, "yaml")
# ---------------------------------------------------------------------------
# Output
# ---------------------------------------------------------------------------
def print_text_results(results: list[ValidationResult]) -> None:
passed = sum(1 for r in results if r.ok)
failed = len(results) - passed
print(f"Validated {len(results)} file(s): {passed} passed, {failed} failed.\n")
for r in results:
icon = "✓" if r.ok else "✗"
print(f" {icon} {r.file}")
for err in r.errors:
print(f" ERROR: {err}")
for warn in r.warnings:
print(f" WARN: {warn}")
if failed > 0:
print(f"\n{failed} file(s) have YAML errors.")
else:
print("\nAll files valid.")
def print_json_results(results: list[ValidationResult]) -> None:
output = {
"total": len(results),
"passed": sum(1 for r in results if r.ok),
"failed": sum(1 for r in results if not r.ok),
"results": [r.to_dict() for r in results],
}
print(json.dumps(output, indent=2, ensure_ascii=False))
# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------
def _build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Validate YAML frontmatter in markdown files or pure YAML files.",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
source = parser.add_mutually_exclusive_group(required=True)
source.add_argument("--dir", type=str, help="Directory to scan recursively for .md files")
source.add_argument("--file", type=str, help="Single file to validate")
parser.add_argument("--require", action="append", default=[], metavar="FIELD",
help="Require this field in frontmatter (repeatable)")
parser.add_argument("--json", action="store_true", dest="json_output",
help="Output results as JSON")
parser.add_argument("--yaml-only", action="store_true",
help="Treat input as pure YAML (not markdown with frontmatter). "
"Use for .yaml/.yml files like manifest.yaml.")
return parser
def _validate_single(path_str: str, require: list[str], yaml_only: bool) -> list[ValidationResult]:
fp = Path(path_str)
if not fp.exists():
print(f"Error: File not found: {fp}", file=sys.stderr)
sys.exit(2)
if yaml_only or fp.suffix in (".yaml", ".yml"):
return [validate_yaml_file(fp, require)]
return [validate_markdown_file(fp, require)]
def _validate_directory(dir_str: str, require: list[str]) -> list[ValidationResult]:
dp = Path(dir_str)
if not dp.is_dir():
print(f"Error: Directory not found: {dp}", file=sys.stderr)
sys.exit(2)
md_files = sorted(dp.rglob("*.md"))
yaml_files = sorted(dp.rglob("*.yaml")) + sorted(dp.rglob("*.yml"))
if not md_files and not yaml_files:
print(f"No .md or .yaml files found under {dp}", file=sys.stderr)
sys.exit(2)
results = [validate_markdown_file(md, require, dp) for md in md_files]
results += [validate_yaml_file(yf, require, dp) for yf in yaml_files]
return results
def main() -> None:
args = _build_parser().parse_args()
if args.file:
results = _validate_single(args.file, args.require, args.yaml_only)
else:
results = _validate_directory(args.dir, args.require)
if args.json_output:
print_json_results(results)
else:
print_text_results(results)
sys.exit(0 if all(r.ok for r in results) else 1)
if __name__ == "__main__":
main()
Related skills
How it compares
Pick cs-onboard over generic project-init skills when the goal is CodeStable-specific `.codestable/` layout rather than a language framework boilerplate.
FAQ
What does cs-onboard do?
把新仓库或有零散文档的仓库接入 CodeStable 体系,两条路径自动判断:空仓库从零搭骨架,已有文档走审计 + 迁移映射。触发:用户说"在这个项目里用 CodeStable"、"搭 CodeStable 结构"、"初始化 CodeStable"、"迁移到 CodeStable"。.
When should I use cs-onboard?
User asks about cs onboard or related SKILL.md workflows.
Is cs-onboard safe to install?
Review the Security Audits panel on this page before installing in production.