Compare commits
12
Commits
f7b7740829
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b47dcf2ff2 | ||
|
|
4ec81e253f | ||
|
|
f2723b8c5d | ||
|
|
f1ea057eb4 | ||
|
|
44a2215940 | ||
|
|
a473075f4d | ||
|
|
fdc54547f5 | ||
|
|
94fa94baef | ||
|
|
bc66afda33 | ||
|
|
9ec5bf1c52 | ||
|
|
0ad31c7a52 | ||
|
|
243bbf1db1 |
@@ -3,6 +3,7 @@
|
||||
|
||||
# 本地 runtime 发现入口(软链到 skills/le-meta-skill,属于个人环境配置)
|
||||
.agents/
|
||||
.zcode/
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
@@ -20,3 +21,10 @@ __pycache__/
|
||||
# 环境与密钥(skill 零密钥底线)
|
||||
.env
|
||||
.env.*
|
||||
|
||||
|
||||
# le-app-design:trigger_eval.py 每次运行会重建 reports/trigger-eval.json(生成物,按需重跑即得,不入库)
|
||||
skills/le-app-design/reports/
|
||||
|
||||
# le-spec:同上,trigger-eval.json 为生成物不入库;test-report.md 为交付证据入库
|
||||
skills/le-spec/reports/trigger-eval.json
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
> *「把重复出现的方法,变成可触发、可验证、可维护的 Skill。」*
|
||||
|
||||
MetaSkill 是 Leroy 的个人 Skill 仓库:**le-meta-skill**(Skill 工坊)与其实战技能 **le-wechat-article**(公众号写作工坊)、**le-gpt-image**(无 Key GPT Image 生图工坊)。ZCode / Claude Code / Codex 通用。
|
||||
MetaSkill 是 Leroy 的个人 Skill 仓库:**le-meta-skill**(Skill 工坊)与其实战技能 **le-wechat-article**(公众号写作工坊)、**le-gpt-image**(无 Key GPT Image 生图工坊)、**le-app-design**(统一 App 设计工坊)。ZCode / Claude Code / Codex 通用。
|
||||
|
||||
## 仓库结构
|
||||
|
||||
@@ -19,10 +19,15 @@ skills/
|
||||
│ ├── references/ # 6 篇:五类型路由·排版规则·审读手册(含合规)·视觉生产·作者画像·常用事实卡
|
||||
│ ├── scripts/ # check_article(硬规则自检)/ md2wechat(3 骨架×4 色板转微信 HTML)/ publish_draft(草稿箱同步,零依赖)
|
||||
│ └── evals/ reports/
|
||||
└── le-gpt-image/ # 无 Key GPT Image 生图工坊
|
||||
├── SKILL.md # 工作流:接单→停点确认→生成交付;能力边界 / 尺寸质量速查 / 故障排查
|
||||
├── references/ # prompt-craft:结构公式·文字渲染·摄影/插画/信息图·中文东方风格·改图指令·反模式
|
||||
└── scripts/ # gpt_image.py(零依赖:Codex 凭证直连内部图像通道,token 自动刷新)
|
||||
├── le-gpt-image/ # 无 Key GPT Image 生图工坊
|
||||
│ ├── SKILL.md # 工作流:接单→停点确认→生成交付;能力边界 / 尺寸质量速查 / 故障排查
|
||||
│ ├── references/ # prompt-craft:结构公式·文字渲染·摄影/插画/信息图·中文东方风格·改图指令·反模式
|
||||
│ └── scripts/ # gpt_image.py(零依赖:Codex 凭证直连内部图像通道,token 自动刷新)
|
||||
└── le-app-design/ # 统一 App 设计工坊(编排器)
|
||||
├── SKILL.md # S0–S7 流水线:依赖门→产品理解→视觉探索→设计规范→构建→审查→迭代→验收;六铁律 + 岗位裁决表
|
||||
├── references/ # pipeline(七阶段细则)· runtimes(四 runtime 适配矩阵)
|
||||
├── scripts/ # check_deps(硬依赖门)/ log_call(调用证据账本)/ evidence_report(验收门),纯标准库
|
||||
└── evals/ # 触发用例(可回归的词面冒烟)
|
||||
```
|
||||
|
||||
## le-meta-skill —— Skill 工坊
|
||||
@@ -85,6 +90,21 @@ skills/
|
||||
|
||||
安全边界:凭证只读不打印不外发;内部接口无稳定性契约(native 失效可切 responses 备用通道);内容政策拒绝即停,不换措辞绕过。
|
||||
|
||||
## le-app-design —— 统一 App 设计工坊
|
||||
|
||||
一个入口(「帮我设计这个 App」)跑完产品理解 → 视觉探索 → 设计规范 → Web 高保真原型 → 设计审查 → 优化迭代 → 交付。它不是借鉴了三个开源 Skill 的新 Skill,而是**真正集成并调用**三个上游的编排器(v1.1 起 frontend-design 先定方向、ui-ux-pro-max 后编系统——方向由题材决定,不由品类目录决定):
|
||||
|
||||
- **ui-ux-pro-max**(MIT):`search.py` CLI 真跑 `--design-system --persist` 生成 MASTER.md,UX/可访问性/触控规范走数据检索——设计规范的硬约束来源
|
||||
- **frontend-design**(Apache-2.0, Anthropic):经宿主 Skill 工具装载(或 Read 原文全文,一等路径)——视觉方向与品牌差异化的 design lead
|
||||
- **impeccable**(Apache-2.0, pbakaus):`impeccable` CLI(context / detect 62 规则 / critique-storage)+ playbook 装载照做——审查、精修与质量底线
|
||||
- **三个机制保证「真集成」可验证**:`check_deps.py` 硬依赖门(缺任何一个即终止并报安装指引)、`log_call.py` 证据账本(每次调用落 calls.jsonl,含装载文件的 sha256)、`evidence_report.py` 验收门(按 `--require` 口径判参与的上游各有 ≥1 条履职成功调用才许宣称完成)。验证不靠静态示例——**每次真实运行现场再生证据**;创建当日已三跑全过验收门(E2E + 同题双轮顺序对比 21/36→24/36、品类默认特征 2→0),过程记录见 git 提交 243bbf1 / 0ad31c7 / bc66afd。
|
||||
|
||||
触发方式:「帮我设计一个 App」「做一个高保真原型」「把首页和详情页这些界面设计出来」。点名单个上游(「用 impeccable 审查」)不触发,直连对应 Skill。
|
||||
|
||||
依赖安装:三个上游需装到 `~/.agents/skills/`(Codex 侧另链 `~/.codex/skills/`),详见 [skills/le-app-design/README.md](skills/le-app-design/README.md)。不 vendoring、不修改上游(归属见其 NOTICE)。
|
||||
|
||||
安全边界:上游目录只读;停点 1(设计方向)与停点 2(修复范围)为回合终止点,「一口气跑完」仅豁免停点、不豁免铁律与验收门。
|
||||
|
||||
## 安装
|
||||
|
||||
三个 runtime 读的都是同一套 SKILL.md 开放格式,脚本只用 python3 标准库。把要装的 skill 软链到对应位置即可:
|
||||
@@ -99,7 +119,7 @@ mkdir -p ~/.claude/skills && ln -s <克隆路径>/skills/le-wechat-article ~/.cl
|
||||
mkdir -p ~/.codex/skills && ln -s <克隆路径>/skills/le-wechat-article ~/.codex/skills/le-wechat-article
|
||||
```
|
||||
|
||||
装 le-meta-skill、le-gpt-image 同理,把路径换成对应 skill 目录。
|
||||
装 le-meta-skill、le-gpt-image 同理,把路径换成对应 skill 目录。装 le-app-design 还需先装三个上游依赖(ui-ux-pro-max / frontend-design / impeccable),并用其 `scripts/check_deps.py` 验证三方全绿。
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# NOTICE — 第三方依赖归属
|
||||
|
||||
le-app-design 是一个编排器 Skill:运行时按各自原生机制调用以下三个上游
|
||||
Skill(CLI 执行 / 原文装载),不分发、不内嵌、不修改它们的任何内容。
|
||||
本仓库遵循各上游许可证条款,特此归属:
|
||||
|
||||
## ui-ux-pro-max
|
||||
- 上游: https://github.com/nextlevelbuilder/ui-ux-pro-max-skill
|
||||
- 许可证: MIT © nextlevelbuilder
|
||||
- 调用方式: 执行其 scripts/search.py(设计系统生成与域检索),读取其 data/*.csv 经由该 CLI
|
||||
- 本仓库不包含其源码副本
|
||||
|
||||
## frontend-design
|
||||
- 上游: https://github.com/anthropics/skills (skills/frontend-design)
|
||||
- 许可证: Apache-2.0 © Anthropic PBC(其 SKILL.md 附 LICENSE.txt 全文)
|
||||
- 调用方式: 宿主 Skill 工具装载,或 Read 其 SKILL.md 原文全文进执行上下文
|
||||
- 本仓库不包含其源码副本
|
||||
|
||||
## impeccable
|
||||
- 上游: https://github.com/pbakaus/impeccable
|
||||
- 许可证: Apache-2.0 © Patrick Bakaus
|
||||
- 调用方式: 执行其 scripts/impeccable CLI(context / detect / critique-storage…),
|
||||
并按其 SKILL.md 路由装载 reference/*.md playbook 照做
|
||||
- 本仓库不包含其源码副本
|
||||
|
||||
---
|
||||
le-app-design 自身以仓库根 LICENSE(MIT, © 2026 Leroy)发布。
|
||||
@@ -0,0 +1,60 @@
|
||||
# le-app-design
|
||||
|
||||
> 统一 App 设计工坊:一个入口完成产品理解 → 视觉探索 → 设计规范 → Web 高保真原型 → 设计审查 → 优化迭代 → 交付。底层**真实集成并调用**三个上游 Skill,本 Skill 只做编排、协调与质量控制。
|
||||
|
||||
**状态**:rigorous 档(一处有意偏离:不设 reports/ 静态证据目录——本 skill 的验证形态是**每次运行现场再生**(依赖门 + 调用账本 + 验收门),静态示例会腐烂且冗余,历史记录由 git 提交承担。2026-11-07 与 Leroy 确认)。ZCode / Claude Code / Codex / OpenCode 通用。
|
||||
|
||||
## 它解决什么问题
|
||||
|
||||
你想要一个 App 从想法变成可交互的高保真原型,只需要说「帮我设计这个 App」。le-app-design 负责检测依赖、给三个上游分派岗位、串起全流程、留下可检查的调用证据:frontend-design 先定有品牌差异性的视觉方向,ui-ux-pro-max 把方向编撰成设计系统并对可访问性/触控等硬约束校验,impeccable 做设计审查与反模式检测并盯着修复。你不需要知道三个上游各自怎么调。
|
||||
|
||||
## 三个上游依赖(必需,非可选)
|
||||
|
||||
| 上游 | 岗位 | 许可证 | 仓库 |
|
||||
|---|---|---|---|
|
||||
| ui-ux-pro-max | 设计规范生成 / UX 与可访问性数据检索 | MIT | github.com/nextlevelbuilder/ui-ux-pro-max-skill |
|
||||
| frontend-design | 视觉创意 / 品牌差异化(Anthropic) | Apache-2.0 | github.com/anthropics/skills |
|
||||
| impeccable | 设计审查 / 62 规则反模式检测 / 精修 | Apache-2.0 | github.com/pbakaus/impeccable |
|
||||
|
||||
本 Skill 不 vendoring、不修改、不复制三个上游的任何内容,只按其原生机制调用(CLI 执行 / 原文装载)。归属详见 [NOTICE](NOTICE)。
|
||||
|
||||
## 依赖安装
|
||||
|
||||
三份都装到 `~/.agents/skills/`(首选,impeccable 自引用该路径):
|
||||
|
||||
```bash
|
||||
# 示例:ui-ux-pro-max
|
||||
git clone --depth 1 https://github.com/nextlevelbuilder/ui-ux-pro-max-skill /tmp/uiux
|
||||
ln -s /tmp/uiux/.claude/skills/ui-ux-pro-max ~/.agents/skills/ui-ux-pro-max
|
||||
# frontend-design: git clone anthropics/skills → skills/frontend-design
|
||||
# impeccable: git clone pbakaus/impeccable → 整目录链接
|
||||
```
|
||||
|
||||
Codex 侧还需链接到 `~/.codex/skills/`。装完验证:
|
||||
|
||||
```bash
|
||||
python3 <本目录>/scripts/check_deps.py
|
||||
```
|
||||
|
||||
三方全绿才可使用;缺任何一个,触发本 Skill 时会明确报缺并终止,不会假装集成成功。
|
||||
|
||||
## 触发方式
|
||||
|
||||
- 帮我设计一个习惯打卡 App,要高保真原型
|
||||
- 把这个产品想法做成可交互的 UI 原型
|
||||
- 给我的工具类产品做一轮完整的设计 + 审查 + 改进
|
||||
- design an app prototype for …
|
||||
|
||||
不触发:点名单个上游(「用 impeccable 审查」)、公众号文章/海报排版、只要一张配图、纯功能开发。
|
||||
|
||||
## 核心机制(为什么这是真集成)
|
||||
|
||||
- **硬依赖门** `scripts/check_deps.py`:按依赖各自形态做活性检查——pro-max 真跑一次检索、impeccable 跑二进制 `--version`、frontend-design 校验原文完整可读(含 sha256)。缺则终止。
|
||||
- **证据账本** `scripts/log_call.py`:每次对上游的调用(CLI 执行与原文装载)都落 `calls.jsonl`——时间、命令、退出码、stdout 摘要、装载文件的 sha256。没进账本的调用视为没发生。
|
||||
- **验收门** `scripts/evidence_report.py`:三方各有 ≥1 条成功调用才判 PASS;否则 FAIL 并列出缺口,禁止把未完成的流程标记为成功。
|
||||
|
||||
## 安全边界
|
||||
|
||||
- 三个上游目录只读;本 Skill 的任何流程都不修改它们。
|
||||
- 停点 1(设计方向确认)与停点 2(修复范围确认)会停下来等你;说「一口气跑完」可豁免停点,但豁免不了铁律与验收门。
|
||||
- 交付产物全部落盘(运行目录 `le-app-design-runs/<id>/`),包括审查报告与集成证据,可复查。
|
||||
@@ -0,0 +1,140 @@
|
||||
---
|
||||
name: le-app-design
|
||||
description: |
|
||||
Le App Design —— 统一 App 设计工坊:只叫这一个名字,自动完成「产品理解 → 视觉探索 → 设计规范 → Web 高保真原型 → 设计审查 → 优化迭代 → 交付」。底层真实集成并调用三个上游 Skill——frontend-design(视觉创意与品牌差异化,先定方向)、ui-ux-pro-max(设计规范系统化与 UX 硬约束校验,后编系统)、impeccable(设计审查/精修/反模式检测);本 Skill 只做依赖检测、任务分配、执行协调与质量控制,绝不替代三者任何能力。
|
||||
何时触发:想要完整的 App / 产品 UI 设计流程——"帮我设计一个 App""做一个高保真原型""从想法到可交互原型""给这个产品做 UI 设计和原型""design an app";或指定若干界面做设计——"把首页和详情页这些界面设计出来""做移动端的页面/界面设计""App 界面改版"。
|
||||
不要触发:点名单个上游 Skill 的直接请求(直连该上游即可);写作与排版类任务;单张配图生成;纯功能开发与排障(无设计诉求)。
|
||||
metadata:
|
||||
author: Leroy
|
||||
version: 1.2.1
|
||||
runtimes: zcode, claude-code, codex, opencode
|
||||
requires:
|
||||
- "ui-ux-pro-max (MIT, nextlevelbuilder/ui-ux-pro-max-skill)"
|
||||
- "frontend-design (Apache-2.0, anthropics/skills)"
|
||||
- "impeccable (Apache-2.0, pbakaus/impeccable)"
|
||||
---
|
||||
|
||||
# Le App Design —— 统一 App 设计工坊
|
||||
|
||||
一个入口,三个真依赖。用户只说「帮我设计这个 App」,本 Skill 编排三个上游 Skill 完成从理解到交付的全流程。
|
||||
|
||||
## 铁律(违反任何一条即本次运行失败)
|
||||
|
||||
1. **真调用,不冒充。** 三个上游必须以其原生形态被实际调用:ui-ux-pro-max 跑它的 `search.py` CLI;impeccable 跑它的 `impeccable` CLI 并按其 SKILL.md 路由装载 reference/*.md playbook 照做;frontend-design 经宿主 Skill 工具装载(或 Read 原文全文)。禁止只引用名字、模仿理念、用自写流程替代。
|
||||
2. **依赖门不过,不许动工。** S0 检测到任何一个上游缺失/失活,立即向用户报告缺什么、怎么装,终止本次运行。不许跳过、不许假装集成完成。
|
||||
3. **调用必记账。** 每次对上游的调用(含 playbook/文档装载)都经 `log_call.py` 落入本次运行目录的 `calls.jsonl`。没进账本的调用视为没发生。
|
||||
4. **交付必过验收门。** S7 跑 `evidence_report.py`,门按任务类型的 `--require` 集合判:完整流程要求三方各 ≥1 条**履职阶段**成功调用(pro-max 计 S3- 前缀,防 S2 调研混门);轻量路由只要求参与该路由的上游。不过门不许宣称完成。
|
||||
5. **不改上游。** 三个上游目录只读(调用、装载),任何修改都不属于本 Skill 的职责。
|
||||
6. **用户 brief 原话 > 一切。** 三个上游都自认 brief 优先,本 Skill 同样:brief 钉死的方向照办,冲突记入决策日志。
|
||||
|
||||
## 岗位分工(冲突时按此裁决)
|
||||
|
||||
| 维度 | 主责 | 裁决 |
|
||||
|---|---|---|
|
||||
| 视觉方向、品牌差异化、排版个性 | frontend-design(design lead) | 美学轴自由时听它的 POV,拒绝模板脸 |
|
||||
| 设计规范、可访问性、触控/平台规则、数据支撑选型 | ui-ux-pro-max | **硬约束**:WCAG/HIG/MD 类规则赢过美学偏好 |
|
||||
| 质量底线、验收标准、反模式 | impeccable | craft-floor 的 ban 是红线(如 eyebrow 禁令、嵌套卡片必错) |
|
||||
| 产品事实、范围、语气 | 用户 brief | 最高优先级 |
|
||||
|
||||
## 工作流 S0–S7
|
||||
|
||||
细则(每阶段的动作、命令、产物、验收)见 [pipeline.md](references/pipeline.md);跨 runtime 适配见 [runtimes.md](references/runtimes.md)。设 `<SKILL>` = 本 Skill 目录,`<RUN>` = 运行目录(默认 `<项目根>/le-app-design-runs/<时间戳>-<slug>/`,下同)。
|
||||
|
||||
### S0 依赖门(每次触发必跑,轻量任务也不例外)
|
||||
|
||||
```bash
|
||||
python3 <SKILL>/scripts/check_deps.py --out <RUN>/deps.json
|
||||
```
|
||||
|
||||
三方解析 + 活性检查(pro-max 真跑一次检索、impeccable 跑 `--version`、frontend-design 校验原文完整可读)。非零退出 → 停止并报告缺失项与安装指引,本次运行终止。
|
||||
|
||||
### S1 产品理解
|
||||
|
||||
产出 `<RUN>/BRIEF.md`:**项目代号**(供 S3 的 `-p` 与目录命名)、产品是什么、给谁、核心任务、平台与形态、impeccable 四模式归属(Persuade / Operate / Read / Experience)。brief 信息不足且影响方向 → 问用户;足够 → 写明假设直接做。
|
||||
|
||||
### S2 视觉探索 — frontend-design(方向先行)
|
||||
|
||||
1. 宿主有 Skill 工具(ZCode / Claude Code)→ 用它装载 `frontend-design`;没有 → Read `<DEP_FD>/SKILL.md` 全文进上下文(一等路径,不是降级)。
|
||||
2. 装载后立即记账:`python3 <SKILL>/scripts/log_call.py frontend-design --run-dir <RUN> --load <DEP_FD>/SKILL.md`
|
||||
3. 按其「两遍法」产出 `<RUN>/design-plan.md`:从题材世界出发定艺术方向——色板(4–6 个具名 hex)、字体与角色、布局概念(一句话 + ASCII)、设计原则;对照它的 AI 味特征清单逐条自检,修订写明改了什么、为什么。**此时不看任何品类匹配结果——方向由题材与 POV 决定,不由目录里有什么决定。**
|
||||
4. (可选调研模式)方向成形前可用 pro-max 的 `--domain style|color|typography` 检索选项当情绪板(经 log_call 记账、note 标 S2-research)——只供参考,不做决定,不生成 MASTER。
|
||||
|
||||
### S3 设计规范 — ui-ux-pro-max(系统后行)
|
||||
|
||||
方向已定,本阶段把它**编撰**成可执行的规范并对硬约束校验:
|
||||
|
||||
```bash
|
||||
# 用已定方向的关键词驱动系统生成(不是品类词),dials 按方向调
|
||||
python3 <SKILL>/scripts/log_call.py ui-ux-pro-max --run-dir <RUN> --note S3-design-system -- \
|
||||
python3 <DEP_UIUX>/scripts/search.py "<方向关键词,如 exercise-book hand-drawn sticker>" \
|
||||
--design-system --persist -p "<项目名>" --output-dir <项目根>
|
||||
```
|
||||
|
||||
两条规则:
|
||||
- **硬约束否决权**:frontend-design 定的 hex/字号若对比度、触控、平台规范不达标,以 pro-max 数据(WCAG/HIG/MD)为准**改值不改方向**;逐项校验(`--domain ux`、`--domain typography` 等)全部经 log_call。
|
||||
- **目录回弹降级**:若 pro-max 风格目录(51 个活跃风格)对已定方向匹配失败——生成结果明显回弹到品类默认——则**不采用该 MASTER**,降级为「design-plan 的 token 表即规范 + pro-max 逐项校验补硬约束」,在 design-plan 决策记录里写明降级与理由。方向的独特性优先于系统的便利性。
|
||||
|
||||
### 停点 1(回合终止点)
|
||||
|
||||
向用户展示 BRIEF + design-plan(设计方向)+ MASTER 要点或降级说明,等确认或调整后再进构建。用户已明确说「一口气跑完」→ 豁免本停点,继续。**本停点必须终止回合等待用户,不得自答自继续。**
|
||||
|
||||
### S4 高保真构建(a 首屏 → 停点 1.5 → b 全量)
|
||||
|
||||
1. Read `<DEP_IMPECCABLE>/reference/craft-floor.md`(装载即调用)并记账。
|
||||
2. **S4a 首屏**:先只构建主界面一屏(真实内容 + 规范 token + design-plan 个性),390×844 截图。
|
||||
3. **停点 1.5(首屏确认,回合终止点)**:把首屏截图发到对话,请用户确认「方向落地长这样」再继续——这一站消除盲盒:成品第一次见面不再是 S5 之后。豁免时跳过停顿,**但截图仍必须发出**。
|
||||
4. **S4b 全量**:其余屏 + 完整交互(390 主档 + 375 兼容档截图;移动 Web 任务免桌面档)。显隐/布局断言用渲染事实(computed display / boundingRect),不信 hidden 属性。
|
||||
|
||||
### S5 设计审查 — impeccable
|
||||
|
||||
1. `log_call.py impeccable -- sh <DEP_IMPECCABLE>/scripts/impeccable context --target <主文件>`,按其指令行事。
|
||||
2. 按其 SKILL.md 路由装载 `reference/critique.md`(+ 需要的 audit/craft-floor 等)并照做——critique 要求**双独立 sub-agent**(Assessment A 设计评审 / Assessment B detect+浏览器证据):ZCode/Claude Code 用 Agent 工具并行派两个;Codex 按 critique.md 的 Codex 门问询;无 sub-agent 的 runtime 按其降级规则出横幅。playbook 装载逐一记账。
|
||||
3. Assessment B 真跑:`log_call.py impeccable -- sh <DEP_IMPECCABLE>/scripts/impeccable detect --json <文件>`;可浏览器时按 playbook 走 overlay/截图证据。
|
||||
4. 按 critique.md 完整出报告(启发式评分表、persona 红旗、P0–P3、快照持久化 + trend)。
|
||||
|
||||
### 停点 2(回合终止点)
|
||||
|
||||
展示审查报告**并附修复前后关键截图**(不是纯文字报告——用户确认修复范围时应已见过成品),四选一:全部 / Top 3 / 仅 P0-P1 / 保持现状(现状=跳过 S6 直接 S7,报告照出)。「一口气跑完」豁免同上;豁免时默认修 P0+P1,且审查报告末尾必须补字面行 `Questions skipped: 停点2经用户豁免(默认修复 P0+P1)`——这是 impeccable critique 契约的收尾要求,漏了即该次 critique 不完整。
|
||||
|
||||
### S6 优化迭代
|
||||
|
||||
按确认的范围修复:P0/P1 必修;修完复跑 `detect --json` 至 0 findings(或记明剩余项与理由);复截图核对视觉。涉及大改方向的回到 S3 重走(罕见)。
|
||||
|
||||
### S7 交付 + 验收门
|
||||
|
||||
```bash
|
||||
# 完整流程(默认三方门,pro-max 计履职前缀)
|
||||
python3 <SKILL>/scripts/evidence_report.py --run-dir <RUN> --out <RUN>/INTEGRATION-REPORT.md \
|
||||
--require "ui-ux-pro-max:S3-,frontend-design:S2-,impeccable" \
|
||||
--artifact <原型主文件> --artifact <RUN>/shot-*.png --artifact <RUN>/design-plan.md
|
||||
# 系统路径(非降级)另加 --artifact design-system/<slug>/MASTER.md(slug 跑完 S3 后 ls design-system/ 确认)
|
||||
```
|
||||
|
||||
交付物 = 原型(可打开可交互)+ BRIEF/design-plan/MASTER(或降级说明)+ 审查报告与快照 + INTEGRATION-REPORT。报告 FAIL(门或产物缺失)→ 不许宣称完成,如实说明缺什么。
|
||||
|
||||
## 轻量任务路由(含各自的验收门口径)
|
||||
|
||||
- **仅审查/体检**(「帮我审一下这个原型的设计」):S0 → S5(完整 critique:装载 critique.md + 双 sub-agent + detect + 快照)→ 停点 2 →(S6 按所选范围)→ S7。验收门 `--require "impeccable"`。
|
||||
- **局部修改**(「把这个页面间距调好」):S0 → S2 快速版(涉及视觉决策时:装载 frontend-design + 记账,方向口头确认即可,无 design-plan 全文档)→ S4′(craft-floor 装载 + 就地修改 + 渲染事实断言 + 复截图)→ S5′(detect --json + 复截对比,不走完整 critique)→ S7。验收门 `--require "impeccable"`(走了 S2 快速版则加 `,frontend-design`)。
|
||||
- 三上游经 check_deps 解析后始终可调用;轻量路由是省阶段,不是换实现——省掉的阶段不要求调用,但**不许伪造调用记录凑门**。
|
||||
|
||||
## 失败模式与停手点
|
||||
|
||||
- 依赖缺失/失活:S0 即停,报缺失项与安装指引(见 README「依赖安装」)。
|
||||
- 上游 CLI 中途失败:重试一次;仍失败 → 记账(账本保留失败记录)、报告用户、终止或按用户指示降级到其余上游可覆盖的范围,并在交付物中明确标注缺口。
|
||||
- Chrome/浏览器不可用:截图核对改为声明 fallback(craft-floor 逐条人工核对),在报告中注明。
|
||||
- 用户点名单一上游:不触发本 Skill(负触发已在 description 声明)。
|
||||
|
||||
## 反例黑名单
|
||||
|
||||
- 不要在账本外「顺手」直接调上游——没记账的调用等于没发生。
|
||||
- 不要把三个上游的文档内容抄进本 Skill 或其 references 来「省一次装载」。
|
||||
- 不要因为某上游慢/临时失败就用自己的设计判断顶替其岗位并照常宣称完成。
|
||||
- 不要在停点自问自答继续跑;豁免只能来自用户明确授权。
|
||||
- 不要检测到依赖缺失还继续产出「设计」——那是没有集成的假成功。
|
||||
|
||||
## Reference Map
|
||||
|
||||
- 七阶段细则与验收:[pipeline.md](references/pipeline.md)
|
||||
- 跨 runtime 适配与已知限制:[runtimes.md](references/runtimes.md)
|
||||
- 依赖安装与许可证:[README.md](README.md)、[NOTICE](NOTICE)
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"_说明": "le-app-design 触发用例。family 标注场景族,便于回归分析。",
|
||||
"recommended_threshold": 0.3,
|
||||
"negative_patterns": ["用 impeccable", "impeccable 审查", "ui-ux-pro-max", "查个配色", "公众号", "写一篇"],
|
||||
"should_trigger": [
|
||||
{"text": "帮我设计一个习惯打卡 App,要能看的高保真原型", "family": "full_design_cn"},
|
||||
{"text": "把这个产品想法做成可交互的 UI 原型,从设计到成品", "family": "full_design_cn"},
|
||||
{"text": "design an app for my recipe startup, high-fidelity prototype", "family": "full_design_en"},
|
||||
{"text": "给我的工具类产品做一轮完整的设计:规范、视觉、原型加审查", "family": "full_pipeline"},
|
||||
{"text": "我要做个妈妈育儿 App,帮我把首页和详情页设计出来", "family": "app_screens"},
|
||||
{"text": "用 le-app-design 把这个 idea 变成原型", "family": "explicit_name"}
|
||||
],
|
||||
"should_not_trigger": [
|
||||
{"text": "用 impeccable 审查一下这个页面", "family": "single_upstream_direct"},
|
||||
{"text": "ui-ux-pro-max 帮我查个配色", "family": "single_upstream_direct"},
|
||||
{"text": "帮我写一篇公众号文章", "family": "other_le_skill"},
|
||||
{"text": "给我生成一张 App 封面图", "family": "image_only"},
|
||||
{"text": "这个按钮的点击事件没反应,帮我修一下", "family": "pure_dev"}
|
||||
],
|
||||
"near_neighbor": [
|
||||
{"text": "把这个页面布局调好看一点", "family": "local_tweak", "note": "局部修改走轻量路由(S0+S4′+S5′),仍触发但流程裁剪"}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
# 七阶段流水线细则
|
||||
|
||||
SKILL.md 是路由,本文是执行手册。每阶段给出:输入 → 动作(含确切命令)→ 产物 → 验收。命令中 `<SKILL>` = le-app-design 目录,`<RUN>` = 本次运行目录,`<DEP_UIUX>` / `<DEP_FD>` / `<DEP_IMP>` = S0 解析出的三个上游路径(读 `<RUN>/deps.json` 的 `deps.*.path`)。
|
||||
|
||||
## S0 依赖门
|
||||
|
||||
**动作**
|
||||
```bash
|
||||
mkdir -p <RUN>
|
||||
python3 <SKILL>/scripts/check_deps.py --out <RUN>/deps.json
|
||||
```
|
||||
|
||||
**验收**:exit 0 且 deps.json 的 `ok=true`。任一依赖 `alive=false` → 向用户输出缺失项、原因(`check` 字段)与安装指引(`install` 字段),**终止运行**。deps.json 是后续所有阶段的路径来源,不要手写上游路径。
|
||||
|
||||
## S1 产品理解
|
||||
|
||||
**动作**:从用户输入提炼并写 `<RUN>/BRIEF.md`,包含:
|
||||
|
||||
- 产品:一句话说清这是什么、解决什么
|
||||
- 受众与场景:谁、在什么场景用(决定 impeccable 模式与 frontend-design 的取材世界)
|
||||
- 核心任务:用户打开它要完成的最重要 1–3 件事
|
||||
- 平台形态:移动 Web(默认 390×844)/ 桌面 / 响应式;明确「Web 高保真原型」是交付形态
|
||||
- 模式归属:Persuade / Operate / Read / Experience 四选一(impeccable 按表面而非产品选模式:App 内页=Operate,落地页=Persuade,阅读页=Read,作品展示=Experience)
|
||||
- 范围:本次做哪些屏/页面,明确排除什么
|
||||
|
||||
信息不足且**影响方向**才问用户(用结构化选项,别开放式问卷);否则写明「假设:…」直接做。多次触达同一项目时先读已有 BRIEF.md,增量更新而非重写。
|
||||
|
||||
**验收**:BRIEF.md 落盘,模式归属有理由。
|
||||
|
||||
## S2 视觉探索 — frontend-design(方向先行)
|
||||
|
||||
**动作**
|
||||
1. 宿主有 Skill 工具 → 调它装载 `frontend-design`;没有 → Read `<DEP_FD>/SKILL.md` 全文(两路等价,装载即调用)。
|
||||
2. 立即记账:`python3 <SKILL>/scripts/log_call.py frontend-design --run-dir <RUN> --note S2-load --load <DEP_FD>/SKILL.md`
|
||||
3. 以其「设计总监」立场产出 `<RUN>/design-plan.md`:
|
||||
- **题材世界**:从产品自己的世界取材(subject matter 是差异化来源),写明方向一句话
|
||||
- **色板**:4–6 个具名 hex(`--纸面 #FDFDF8` 式命名)
|
||||
- **字体**:选型 + 角色,说明为何不是默认脸
|
||||
- **布局**:一句话概念 + 每屏 ASCII 线框 + 对齐策略
|
||||
- **原则**:这个产品独有的 3–5 条
|
||||
- **两遍法第二遍**:对照其「AI 味特征清单」逐条自检;发现默认味 → 写明改了什么、为什么
|
||||
4. (可选调研模式)方向成形前可用 pro-max `--domain style|color|typography` 检索选项当情绪板(记账 note=S2-research)——**只供参考不做决定**,此阶段禁止 `--design-system`。
|
||||
|
||||
**验收**:design-plan.md 四要素齐全 + 自检一节存在 + 记账在案;方向不来自品类匹配。
|
||||
|
||||
## S3 设计规范 — ui-ux-pro-max(系统后行)
|
||||
|
||||
**动作**:把已定方向编撰成规范 + 硬约束校验:
|
||||
|
||||
```bash
|
||||
# 主调用:用方向关键词(不是品类词)驱动系统生成;dials 按方向调
|
||||
python3 <SKILL>/scripts/log_call.py ui-ux-pro-max --run-dir <RUN> --note S3-design-system -- \
|
||||
python3 <DEP_UIUX>/scripts/search.py "<方向关键词>" --design-system --persist \
|
||||
-p "<项目名>" --output-dir <项目根> [--variance N --motion N --density N]
|
||||
|
||||
# 硬约束校验(按需,全部记账):对比度/触控/排版/相关 UX 规则
|
||||
python3 <SKILL>/scripts/log_call.py ui-ux-pro-max --run-dir <RUN> --note S3-ux-check -- \
|
||||
python3 <DEP_UIUX>/scripts/search.py "<如 contrast touch target>" --domain ux
|
||||
```
|
||||
|
||||
**两条规则**:
|
||||
- **硬约束否决权**:frontend-design 定的 hex/字号若不达标(WCAG/HIG/MD 数据为准),**改值不改方向**;每处修改记入 design-plan 决策记录。
|
||||
- **目录回弹降级(操作化判据)**:生成结果命中以下任意两条即判回弹、不采用该 MASTER——①Style 名与方向关键词无词面或语义关联(如「练习本手写」→ Swiss/Helvetica);②色板命中 AI-tell 家族(暖奶油底、默认靛蓝/紫系、近黑+酸绿)且与 design-plan 色板无共享主色;③关键 Effects/Anti-patterns 与方向的手感相反(如手绘方向得到 corporate/rational)。降级后:design-plan 的 token 表即规范 + pro-max 逐项校验补硬约束,决策记录写明命中哪两条。方向的独特性优先于系统的便利性。
|
||||
|
||||
**验收**:MASTER.md 存在且与方向一致,或降级路径已记录;校验检索在账本;design-plan 决策记录含 S3 的改值/降级条目。
|
||||
|
||||
## 停点 1
|
||||
|
||||
把 BRIEF(要点)、design-plan(方向 + ASCII + 色板 + 原则)、MASTER 要点或 S3 降级说明展示给用户,明确请求确认或调整意见,**终止回合**。用户豁免(已说「一口气跑完」)→ 跳过继续。
|
||||
|
||||
## S4 高保真构建(a 首屏 → 停点 1.5 → b 全量)
|
||||
|
||||
**S4a 首屏**
|
||||
1. Read `<DEP_IMP>/reference/craft-floor.md` 并记账:
|
||||
`python3 <SKILL>/scripts/log_call.py impeccable --run-dir <RUN> --note S4-craft-floor --load <DEP_IMP>/reference/craft-floor.md`
|
||||
2. 只构建**主界面一屏**(真实内容 + 规范 tokens + design-plan 个性),390×844 截一张。
|
||||
|
||||
**停点 1.5(首屏确认)**:截图发到对话请用户确认方向落地;豁免时跳过停顿但截图必须发出。**终止回合**;这一站是「盲盒阀门」——成品首见从 S5 之后提前到构建开头。
|
||||
|
||||
**S4b 全量**
|
||||
1. 构建:默认无构建链的 HTML/CSS/JS(单文件优先,可拆 CSS/JS 文件)。硬性要求:
|
||||
- token 来自 MASTER.md(或降级时的 design-plan token 表),不发明规范之外的美学量
|
||||
- 布局与个性按 design-plan;SVG 图标(真图标库或手绘一致笔画),禁 emoji 图标;插画/吉祥物/场景图可按 runtime 用图像生成(Codex=内建 image_gen,边界与产物规则见 [runtimes.md](runtimes.md)),图标系统与精确几何仍走 SVG
|
||||
- craft-floor 全条目施工时遵守(尤其:eyebrow 是 ban;嵌套卡片必错;elevation 声明一次;浏览器表面——selection/caret/scrollbar/focus ring/表格数字——从色板定制)
|
||||
- 交互真实可用(点击/切换/滚动行为写 JS),不是静态画片
|
||||
- pro-max Pre-Delivery 逐条过:44px 触控、4.5:1 对比、focus ring、reduced-motion、375px 无横滚
|
||||
2. 截图口径(统一):**390×844 主档 + 375 兼容档各至少一张**;移动 Web 任务免桌面档(body 居中列已保证桌面不破)。按 craft-floor「一次看一批」:Read 截图找问题 → 修完一批 → 再截。编排器无法亲验截图图像时,交 S5 Assessment A 首个看图。
|
||||
3. **显隐与布局断言用渲染事实(computed display / getBoundingClientRect),不信 hidden 属性**——id/类选择器的 display 会压过 [hidden](实测翻车:两页堆叠渲染而属性检查全绿)。
|
||||
|
||||
**验收**:原型可打开(`file://` 直开无 console error 级问题)、双档截图在 `<RUN>/`、craft-floor 关键项肉眼过。
|
||||
|
||||
## S5 设计审查 — impeccable
|
||||
|
||||
**动作**
|
||||
1. 上下文装载:
|
||||
```bash
|
||||
python3 <SKILL>/scripts/log_call.py impeccable --run-dir <RUN> --note S5-context -- \
|
||||
sh <DEP_IMP>/scripts/impeccable context --target <原型主文件>
|
||||
```
|
||||
按 context 输出的指令行事(它可能要求 init/PRODUCT.md——轻量原型按其 SCOPED_EXISTING_ALLOWED 通道走,不阻塞)。
|
||||
2. Read `<DEP_IMP>/reference/critique.md` 并记账(`--load`)。**按 critique.md 原文执行**,特别注意其 Hard Invariants:
|
||||
- 双独立 sub-agent:Assessment A(设计评审)/ Assessment B(detect + 浏览器证据)。ZCode/Claude Code 用 Agent 工具并行派发,两者互不可见对方输出;Codex 有 spawn_agent 先问一次;都没有才单上下文顺序跑并打 `⚠️ DEGRADED` 横幅
|
||||
- **派发模板要点**(子代理看不到主上下文,prompt 必须自带全部材料):
|
||||
- A 传:目标文件路径、BRIEF/design-plan 路径、全部截图路径(要求「真正看图」)、四模式与受众摘要、输出契约(specificity/十启发式表/认知负荷/情绪/P0-P3/persona)
|
||||
- B 传:经 log_call 包裹的 detect 命令全文(注明 exit 0=clean / 2=findings 属成功)、截图清点、误报核对要求(对源码实算)、fallback signal 原文案
|
||||
- B 的 CLI 扫描:
|
||||
```bash
|
||||
python3 <SKILL>/scripts/log_call.py impeccable --run-dir <RUN> --note S5-detect -- \
|
||||
sh <DEP_IMP>/scripts/impeccable detect --json <原型目录或主文件>
|
||||
```
|
||||
(exit 0=clean,2=有 findings;只传 markup 文件)
|
||||
- 浏览器证据:无浏览器自动化时用 headless Chrome 截图代替并在 Run Notes 声明 fallback
|
||||
- 快照持久化 + trend:按 critique.md 的 `critique-storage write/trend` 执行并记账
|
||||
3. 出完整报告:方法横幅、启发式评分表(0–4 ×10,适用 max 规范化)、Design Specificity、P0–P3 优先问题、persona 红旗、Run Notes。
|
||||
|
||||
**验收**:报告完整落 `<RUN>/critique-report.md`(同时按 critique.md 在聊天中交付全文)、detect 结果在账本、快照已写(或记明原因)。
|
||||
|
||||
## 停点 2
|
||||
|
||||
展示审查报告**并附修复前后关键截图**,四选一:全部 / Top 3 / 仅 P0–P1 / 保持现状(跳过 S6 直接 S7,报告照出)。**终止回合**;豁免规则同停点 1(豁免时默认修 P0+P1,并在审查报告末尾补字面行 `Questions skipped: 停点2经用户豁免(默认修复 P0+P1)`,满足 impeccable critique 的收尾契约)。
|
||||
|
||||
## S6 优化迭代
|
||||
|
||||
**动作**:按用户选定范围修(P0/P1 无用户明示保留也必修)→ 复跑 detect(记账,note=S6-redetect)至 exit 0(=0 findings;残留项逐一记明理由)→ 复截图核对修复效果。修复遵守 S4 的同一套约束(token/设计/craft-floor),不许「修着修着」脱离体系。
|
||||
|
||||
**验收**:detect 清零或残留有据;前后截图对比留档 `<RUN>/`。
|
||||
|
||||
## S7 交付 + 验收门
|
||||
|
||||
**动作**
|
||||
```bash
|
||||
# 完整流程:默认三方门,pro-max 计履职前缀(S2-research 不混门)
|
||||
python3 <SKILL>/scripts/evidence_report.py --run-dir <RUN> \
|
||||
--out <RUN>/INTEGRATION-REPORT.md --title "<项目代号> 集成证据" \
|
||||
--require "ui-ux-pro-max:S3-,frontend-design:S2-,impeccable" \
|
||||
--artifact <原型主文件> --artifact <RUN>/shot-<主档>.png --artifact <RUN>/design-plan.md
|
||||
# 仅审查路由:--require "impeccable";局部修改:--require "impeccable[,frontend-design]"
|
||||
# 系统路径(非降级)另加 --artifact design-system/<slug>/MASTER.md;
|
||||
# slug 由上游按 -p 项目代号生成(小写连字符),跑完 S3 用 ls design-system/ 确认实际目录名
|
||||
```
|
||||
|
||||
向用户交付:原型入口(`file://` 路径,可交互)、关键截图、INTEGRATION-REPORT 判定与调用统计、审查分数与修复摘要、运行目录索引。**FAIL 时不宣称完成**,把缺口(哪个门没过/哪个产物缺失)说清楚。
|
||||
|
||||
## 全程通用约束
|
||||
|
||||
- **note 命名规范**(验收门的履职前缀依赖它):`S2-load`(frontend-design 装载)、`S2-research`(可选调研,不计入门)、`S3-design-system` / `S3-ux-check` / `S3-typography-check`(pro-max 履职)、`S4-craft-floor`、`S5-context` / `S5-critique-playbook` / `S5-detect`、`S6-redetect`。
|
||||
- 每次对上游的调用都过 `log_call.py`,`--run-dir` 显式传(缺省会另建 `<时间戳>-run` 目录,账本裂成两份、验收门读空账本 FAIL);失败调用也要留痕。
|
||||
- 不修改三个上游目录的任何文件;PYTHONDONTWRITEBYTECODE 已由脚本注入。
|
||||
- 运行目录是本次任务的唯一工作痕迹区:BRIEF / design-plan / 截图 / 报告全进 `<RUN>`;MASTER.md 在项目根(上游约定位置,slug 跑完 S3 后 `ls design-system/` 确认实际目录名)。
|
||||
- 用户豁免只作用于停点,不作用于铁律与验收门;豁免停点 1.5 时首屏截图仍须发出。
|
||||
@@ -0,0 +1,38 @@
|
||||
# 跨 Runtime 适配
|
||||
|
||||
CLI 调用(check_deps / log_call / 上游两个 CLI)在所有 runtime 完全一致——它们只依赖 python3 与 sh。差异只在两处:frontend-design 怎么装载、critique 的双 sub-agent 怎么派。
|
||||
|
||||
## 适配矩阵
|
||||
|
||||
| Runtime | frontend-design 装载 | critique 双评审 | 备注 |
|
||||
|---|---|---|---|
|
||||
| ZCode | Skill 工具(首选)或 Read 原文 | Agent 工具并行派 A/B 两个子代理 | 全功能路径,E2E 验证即在此环境跑 |
|
||||
| Claude Code | Skill 工具(首选)或 Read 原文 | Task/Agent 工具并行派 A/B | 同上 |
|
||||
| Codex | Read 原文全文(一等路径) | 有 `spawn_agent`:按 critique.md 的 Codex 门**先问用户一次**再派;无则顺序跑 + `⚠️ DEGRADED` 横幅 | 上游需先装进 `~/.codex/skills/`(见 README);**插画资产可用内建 image_gen**(见下节) |
|
||||
| OpenCode | Read 原文全文(一等路径) | 有 sub-agent 工具就派,无则降级横幅 | 同 Codex 逻辑 |
|
||||
| MausBot / 其它 | Read 原文全文(一等路径) | 无 sub-agent 则降级横幅 | 限制:见下 |
|
||||
|
||||
## Codex 的图像生成(image_gen)
|
||||
|
||||
Codex 内建 `image_gen` 工具可直接生成图片,le-app-design 在 Codex 运行时允许用它产出小型插画资产:
|
||||
|
||||
- **适用**:S4 构建需要的插画类资产——吉祥物/角色形象(手绘 SVG 常见「认不出是什么」的翻车)、场景插图、封面图。prompt 必须携带 design-plan 的色板 hex 与题材世界描述,保证与方向一致;生成后按 craft-floor「Real illustration or none」的插图标准对待。
|
||||
- **不适用**:UI 图标系统仍走 SVG(真图标库或手绘一致描边——icons are drawn,要可缩放可主题化);需要精确几何的图形(图表、印章、格线)走 SVG。
|
||||
- 产物落 `<项目>/imgs/`(或 `<RUN>/assets/`)并进 S7 产物清单;生成图随首屏截图一起在停点 1.5 接受视觉复核(错字/形近字/认不出主体即重生成或退回 SVG)。
|
||||
- image_gen 是宿主能力不是上游 Skill,不进调用账本;**非 Codex runtime 不为此引入依赖**——维持 SVG 自绘,宿主另有图像能力(如本机 le-gpt-image)则由用户指示后使用。
|
||||
|
||||
「一等路径」的含义:frontend-design 是纯指导型 Skill,官方机制就是把它的 SKILL.md 全文装进执行上下文并照做。Read 原文 + `log_call.py --load` 记 sha256,与 Skill 工具装载等效且可审计——**不是降级**。降级只发生在 critique 的双评审(impeccable 自己的定义,见其 critique.md Hard Invariants)。
|
||||
|
||||
## 已知限制(如实告知用户,不静默吞掉)
|
||||
|
||||
1. **impeccable detect 只读 web markup**(HTML/JSX 等)。原生 Swift/Kotlin 源码扫不出东西——纯原生方案 critique 走 Assessment A 单评审(这是上游定义的完整方法,非降级)。本 Skill 交付形态固定为 Web 高保真原型,不受影响。
|
||||
2. **impeccable 二进制按平台分发**(本机 darwin-arm64 已带)。换机器跑 check_deps 会立刻发现缺 `scripts/impeccable` 或其 bin/ 子目录——按报告里的安装指引处理;上游 README 说明二进制会在首跑时按平台下载。
|
||||
3. **截图依赖本机 Chrome**。找不到时 S4/S5 的截图核对降级为 craft-floor 逐条人工核对 + 在 INTEGRATION-REPORT 的 Run Notes 声明,不静默跳过。
|
||||
4. **Windows**:impeccable 用 `scripts/impeccable.cmd`,python3 可能叫 `python`——check_deps/liveness 若失败按报告指引调整;其余逻辑不变。
|
||||
5. **上游版本漂移**:check_deps 记录的是路径 + 活性,不锁版本。上游大版本更新导致 playbook 接口变化时(如 critique.md 的命令名变了),S5 会暴露失败——按失败模式处理并报告,不猜测新接口。
|
||||
|
||||
## 路径解析优先级(check_deps 的顺序)
|
||||
|
||||
`$LE_APP_DESIGN_DEP_ROOTS`(冒号分隔,自定义安装位)→ `~/.agents/skills`(首选默认,与 impeccable SKILL.md 自引用路径一致)→ `~/.zcode/skills` → `~/.codex/skills` → `~/.claude/skills`。
|
||||
|
||||
同一机器多份副本时取优先级最高的一份并在 deps.json 记录实际路径;S2/S5 的命令一律用该路径,不硬编码。
|
||||
@@ -0,0 +1,223 @@
|
||||
#!/usr/bin/env python3
|
||||
"""le-app-design 硬依赖门:解析并活性检查三个上游 Skill。
|
||||
|
||||
用法:
|
||||
python3 check_deps.py [--out <RUN>/deps.json] [--quiet]
|
||||
|
||||
解析顺序: $LE_APP_DESIGN_DEP_ROOTS(冒号分隔) > ~/.agents/skills > ~/.zcode/skills
|
||||
> ~/.codex/skills > ~/.claude/skills
|
||||
(优先 ~/.agents/skills:impeccable 的 SKILL.md 自引用该路径。)
|
||||
|
||||
活性检查按依赖形态各自不同——这就是「真实调用形态」的机器定义:
|
||||
ui-ux-pro-max 真跑一次 search.py 检索(stdout 含结果段即活)
|
||||
frontend-design 校验 SKILL.md 原文完整可读(纯文档型 Skill,装载即调用)
|
||||
impeccable 跑其自包含二进制 --version
|
||||
|
||||
全部通过 exit 0;任何缺失/失活 exit 1 并输出明确报告。仅 python3 标准库。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
SKILL_KEY = "le-app-design-dep-check"
|
||||
|
||||
UPSTREAMS = [
|
||||
{
|
||||
"key": "ui-ux-pro-max",
|
||||
"repo": "nextlevelbuilder/ui-ux-pro-max-skill",
|
||||
"license": "MIT",
|
||||
"install": "git clone https://github.com/nextlevelbuilder/ui-ux-pro-max-skill && 拷贝/链接其 .claude/skills/ui-ux-pro-max 到 ~/.agents/skills/ui-ux-pro-max",
|
||||
"kind": "cli-search",
|
||||
},
|
||||
{
|
||||
"key": "frontend-design",
|
||||
"repo": "anthropics/skills",
|
||||
"license": "Apache-2.0",
|
||||
"install": "git clone https://github.com/anthropics/skills && 拷贝/链接 skills/frontend-design 到 ~/.agents/skills/frontend-design",
|
||||
"kind": "doc-guidance",
|
||||
},
|
||||
{
|
||||
"key": "impeccable",
|
||||
"repo": "pbakaus/impeccable",
|
||||
"license": "Apache-2.0",
|
||||
"install": "git clone https://github.com/pbakaus/impeccable && 拷贝/链接到 ~/.agents/skills/impeccable(含 scripts/ 自包含二进制)",
|
||||
"kind": "cli-engine",
|
||||
},
|
||||
]
|
||||
|
||||
FRONTMATTER_MIN_BYTES = 2000 # frontend-design 原文约 10KB;残缺副本在此拦下
|
||||
|
||||
|
||||
def candidate_roots() -> list[Path]:
|
||||
roots: list[Path] = []
|
||||
env = os.environ.get("LE_APP_DESIGN_DEP_ROOTS", "")
|
||||
for part in env.split(os.pathsep):
|
||||
if part.strip():
|
||||
roots.append(Path(part).expanduser())
|
||||
home = Path.home()
|
||||
roots += [
|
||||
home / ".agents" / "skills",
|
||||
home / ".zcode" / "skills",
|
||||
home / ".codex" / "skills",
|
||||
home / ".claude" / "skills",
|
||||
]
|
||||
seen: set[str] = set()
|
||||
unique: list[Path] = []
|
||||
for r in roots:
|
||||
key = str(r)
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
unique.append(r)
|
||||
return unique
|
||||
|
||||
|
||||
def read_frontmatter_name(skill_md: Path) -> str | None:
|
||||
try:
|
||||
text = skill_md.read_text(encoding="utf-8", errors="replace")
|
||||
except OSError:
|
||||
return None
|
||||
m = re.match(r"^---\s*\n(.*?\n)?---", text, re.DOTALL)
|
||||
if not m:
|
||||
return None
|
||||
block = m.group(1) or ""
|
||||
nm = re.search(r"^name:\s*['\"]?([A-Za-z0-9_-]+)", block, re.MULTILINE)
|
||||
return nm.group(1) if nm else None
|
||||
|
||||
|
||||
def resolve(key: str) -> tuple[Path | None, str]:
|
||||
"""返回 (解析到的目录, 说明)。"""
|
||||
for root in candidate_roots():
|
||||
dep_dir = root / key
|
||||
skill_md = dep_dir / "SKILL.md"
|
||||
if skill_md.is_file():
|
||||
name = read_frontmatter_name(skill_md)
|
||||
if name == key:
|
||||
return dep_dir, f"resolved at {dep_dir}"
|
||||
return dep_dir, f"SKILL.md frontmatter name={name!r} != {key!r}(目录在但身份不符)"
|
||||
return None, "not found in any candidate root"
|
||||
|
||||
|
||||
def _run(cmd: list[str], timeout: int = 90) -> subprocess.CompletedProcess:
|
||||
env = dict(os.environ, PYTHONDONTWRITEBYTECODE="1")
|
||||
return subprocess.run(
|
||||
cmd, capture_output=True, text=True, timeout=timeout, env=env
|
||||
)
|
||||
|
||||
|
||||
def liveness(spec: dict, dep_dir: Path) -> tuple[bool, str]:
|
||||
key, kind = spec["key"], spec["kind"]
|
||||
try:
|
||||
if kind == "cli-search":
|
||||
script = dep_dir / "scripts" / "search.py"
|
||||
if not script.is_file():
|
||||
return False, f"missing {script}"
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
proc = subprocess.run(
|
||||
[sys.executable, str(script), "mobile app", "--domain", "style", "-n", "1"],
|
||||
capture_output=True, text=True, timeout=90, cwd=td,
|
||||
env=dict(os.environ, PYTHONDONTWRITEBYTECODE="1"),
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
return False, f"search.py exit={proc.returncode}: {proc.stderr.strip()[:300]}"
|
||||
if "Result" not in proc.stdout:
|
||||
return False, "search.py exit=0 but no result section in stdout"
|
||||
return True, "search.py live query ok"
|
||||
|
||||
if kind == "doc-guidance":
|
||||
skill_md = dep_dir / "SKILL.md"
|
||||
try:
|
||||
data = skill_md.read_bytes()
|
||||
except OSError as e:
|
||||
return False, f"unreadable SKILL.md: {e}"
|
||||
if len(data) < FRONTMATTER_MIN_BYTES:
|
||||
return False, f"SKILL.md only {len(data)} bytes (<{FRONTMATTER_MIN_BYTES}); likely truncated copy"
|
||||
text = data.decode("utf-8", errors="replace")
|
||||
if "name: frontend-design" not in text or "description:" not in text:
|
||||
return False, "SKILL.md frontmatter incomplete"
|
||||
return True, f"original SKILL.md intact ({len(data)} bytes, sha256 {hashlib.sha256(data).hexdigest()[:12]}…)"
|
||||
|
||||
if kind == "cli-engine":
|
||||
launcher = dep_dir / "scripts" / "impeccable"
|
||||
if not launcher.is_file():
|
||||
return False, f"missing launcher {launcher}"
|
||||
proc = _run(["sh", str(launcher), "--version"])
|
||||
if proc.returncode != 0:
|
||||
return False, f"impeccable --version exit={proc.returncode}: {proc.stderr.strip()[:300]}"
|
||||
version = (proc.stdout or "").strip() or (proc.stderr or "").strip()
|
||||
return True, f"impeccable binary ok ({version[:60]})"
|
||||
except subprocess.TimeoutExpired:
|
||||
return False, f"{key} liveness check timed out"
|
||||
except OSError as e:
|
||||
return False, f"liveness check error: {e}"
|
||||
return False, "unknown kind"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description="le-app-design 依赖门")
|
||||
ap.add_argument("--out", help="JSON 结果写入路径(如 <RUN>/deps.json)")
|
||||
ap.add_argument("--quiet", action="store_true", help="只输出摘要一行")
|
||||
args = ap.parse_args()
|
||||
|
||||
result = {
|
||||
"skill": SKILL_KEY,
|
||||
"ts": datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
||||
"roots_searched": [str(r) for r in candidate_roots()],
|
||||
"deps": {},
|
||||
"ok": False,
|
||||
}
|
||||
|
||||
for spec in UPSTREAMS:
|
||||
dep_dir, note = resolve(spec["key"])
|
||||
entry = {
|
||||
"repo": spec["repo"],
|
||||
"license": spec["license"],
|
||||
"kind": spec["kind"],
|
||||
"path": str(dep_dir) if dep_dir else None,
|
||||
"alive": False,
|
||||
"check": note if dep_dir is None else "",
|
||||
"install": spec["install"],
|
||||
}
|
||||
if dep_dir is not None:
|
||||
alive, check = liveness(spec, dep_dir)
|
||||
entry["alive"] = alive
|
||||
entry["check"] = check
|
||||
result["deps"][spec["key"]] = entry
|
||||
|
||||
result["ok"] = all(d["alive"] for d in result["deps"].values())
|
||||
|
||||
if args.out:
|
||||
out_path = Path(args.out).expanduser()
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
out_path.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
|
||||
lines = []
|
||||
for key, d in result["deps"].items():
|
||||
mark = "✅" if d["alive"] else "❌"
|
||||
where = d["path"] or "未找到"
|
||||
lines.append(f"{mark} {key:18} {where}")
|
||||
if not d["alive"]:
|
||||
lines.append(f" 原因: {d['check']}")
|
||||
lines.append(f" 安装: {d['install']}")
|
||||
verdict = "依赖门通过(三方解析且活性正常)" if result["ok"] else "依赖门未通过——禁止继续设计流程"
|
||||
if args.quiet:
|
||||
print(f"{'PASS' if result['ok'] else 'FAIL'}: " + ", ".join(
|
||||
f"{k}={'ok' if v['alive'] else 'MISSING'}" for k, v in result["deps"].items()))
|
||||
else:
|
||||
print("\n".join(lines))
|
||||
print(verdict)
|
||||
if args.out:
|
||||
print(f"JSON: {args.out}")
|
||||
return 0 if result["ok"] else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,171 @@
|
||||
#!/usr/bin/env python3
|
||||
"""le-app-design 验收门:从 calls.jsonl 生成本次运行的集成证据报告。
|
||||
|
||||
用法:
|
||||
python3 evidence_report.py --run-dir <RUN> [--out <RUN>/INTEGRATION-REPORT.md] \
|
||||
[--artifact <路径>]... [--title "任务名"] \
|
||||
[--require "dep[:note前缀],..."]
|
||||
|
||||
验收规则(硬门):
|
||||
默认要求三个上游 ui-ux-pro-max / frontend-design / impeccable 各 ≥1 条成功调用
|
||||
(exec 且 exit_code==0,或 load 且 exit_code==0)。
|
||||
轻量路由用 --require 指定参与该路由的上游子集,如仅审查任务:
|
||||
--require "impeccable"
|
||||
每项可带 note 前缀限定履职阶段,防止非履职调用(如 S2-research 调研检索)
|
||||
混过门,如完整流程的严格口径:
|
||||
--require "ui-ux-pro-max:S3-,frontend-design:S2-,impeccable"
|
||||
--artifact 指定的每个文件必须存在。
|
||||
任一不满足 → 报告标 FAIL,exit 1;全满足 → PASS,exit 0。
|
||||
|
||||
报告内容: 判定横幅、调用统计表、逐条调用清单(时间/依赖/命令/退出码)、
|
||||
产物核验清单、失败与缺口说明。这份报告就是「真实集成」的可检查证据。
|
||||
仅 python3 标准库。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
GATE_DEPS = ["ui-ux-pro-max", "frontend-design", "impeccable"]
|
||||
|
||||
|
||||
def load_records(ledger: Path) -> list[dict]:
|
||||
if not ledger.is_file():
|
||||
return []
|
||||
records = []
|
||||
for line in ledger.read_text(encoding="utf-8", errors="replace").splitlines():
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
rec = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
records.append({"dep": "(unparsable)", "kind": "raw", "exit_code": None, "command": line[:120], "ts": ""})
|
||||
continue
|
||||
records.append(rec)
|
||||
return records
|
||||
|
||||
|
||||
def is_success(rec: dict) -> bool:
|
||||
return rec.get("exit_code") == 0 and rec.get("kind") in ("exec", "load")
|
||||
|
||||
|
||||
def one_line(rec: dict) -> str:
|
||||
cmd = rec.get("command", "")
|
||||
try: # exec 记录的 command 是 JSON 数组字符串
|
||||
parts = json.loads(cmd)
|
||||
if isinstance(parts, list):
|
||||
cmd = " ".join(str(p) for p in parts)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
pass
|
||||
return cmd if len(cmd) <= 160 else cmd[:157] + "..."
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description="le-app-design 验收门")
|
||||
ap.add_argument("--run-dir", required=True)
|
||||
ap.add_argument("--out", default=None, help="报告写入路径,缺省 <run-dir>/INTEGRATION-REPORT.md")
|
||||
ap.add_argument("--artifact", action="append", default=[], help="须存在的交付物路径(可多次)")
|
||||
ap.add_argument("--title", default="le-app-design 集成证据报告")
|
||||
ap.add_argument("--require", default=None,
|
||||
help='门规格 "dep[:note前缀],...";缺省=三个上游全部、不限前缀。轻量路由传子集')
|
||||
args = ap.parse_args()
|
||||
|
||||
# 解析门规格:dep[:note-prefix]
|
||||
gate: dict[str, str | None] = {}
|
||||
if args.require:
|
||||
for item in args.require.split(","):
|
||||
item = item.strip()
|
||||
if not item:
|
||||
continue
|
||||
dep, _, prefix = item.partition(":")
|
||||
gate[dep] = prefix or None
|
||||
else:
|
||||
gate = {dep: None for dep in GATE_DEPS}
|
||||
|
||||
run_dir = Path(args.run_dir).expanduser()
|
||||
records = load_records(run_dir / "calls.jsonl")
|
||||
out_path = Path(args.out).expanduser() if args.out else run_dir / "INTEGRATION-REPORT.md"
|
||||
|
||||
stats = {dep: {"ok": 0, "fail": 0} for dep in gate}
|
||||
for rec in records:
|
||||
dep = rec.get("dep")
|
||||
if dep in stats:
|
||||
stats[dep]["ok" if is_success(rec) else "fail"] += 1
|
||||
|
||||
def gate_ok(dep: str) -> bool:
|
||||
prefix = gate[dep]
|
||||
if prefix is None:
|
||||
return stats[dep]["ok"] >= 1
|
||||
return any(
|
||||
is_success(rec) and str(rec.get("note", "")).startswith(prefix)
|
||||
for rec in records if rec.get("dep") == dep
|
||||
)
|
||||
|
||||
gate_pass = all(gate_ok(dep) for dep in gate)
|
||||
artifacts_missing = [a for a in args.artifact if not Path(a).expanduser().exists()]
|
||||
overall = gate_pass and not artifacts_missing
|
||||
|
||||
lines: list[str] = []
|
||||
lines.append(f"# {args.title}")
|
||||
lines.append("")
|
||||
lines.append(f"- 运行目录: `{run_dir}`")
|
||||
lines.append(f"- 生成时间: {datetime.now(timezone.utc).isoformat(timespec='seconds')}")
|
||||
lines.append(f"- 调用记录: {len(records)} 条(`calls.jsonl`)")
|
||||
lines.append("")
|
||||
lines.append(f"## 判定: {'✅ PASS' if overall else '❌ FAIL'}")
|
||||
lines.append("")
|
||||
lines.append("| 上游 | 成功调用 | 失败调用 | 门(≥1 成功) |")
|
||||
lines.append("|---|---|---|---|")
|
||||
for dep in gate:
|
||||
s = stats[dep]
|
||||
req = f"(限 note={gate[dep]}*)" if gate[dep] else ""
|
||||
lines.append(f"| {dep}{req} | {s['ok']} | {s['fail']} | {'✅' if gate_ok(dep) else '❌ 未被真实调用'} |")
|
||||
lines.append("")
|
||||
|
||||
if artifacts_missing:
|
||||
lines.append("**缺失产物:**")
|
||||
for a in artifacts_missing:
|
||||
lines.append(f"- ❌ `{a}`")
|
||||
lines.append("")
|
||||
elif args.artifact:
|
||||
lines.append("**产物核验:**")
|
||||
for a in args.artifact:
|
||||
lines.append(f"- ✅ `{a}`")
|
||||
lines.append("")
|
||||
|
||||
lines.append("## 调用清单")
|
||||
lines.append("")
|
||||
lines.append("| # | 时间(UTC) | 依赖 | 类型 | 退出码 | 命令/装载 |")
|
||||
lines.append("|---|---|---|---|---|---|")
|
||||
for i, rec in enumerate(records, 1):
|
||||
lines.append(
|
||||
f"| {i} | {rec.get('ts', '')} | {rec.get('dep', '?')} | {rec.get('kind', '?')} "
|
||||
f"| {rec.get('exit_code', '?')} | `{one_line(rec)}` |"
|
||||
)
|
||||
lines.append("")
|
||||
|
||||
if not overall:
|
||||
lines.append("## 缺口说明")
|
||||
lines.append("")
|
||||
for dep in gate:
|
||||
if not gate_ok(dep):
|
||||
prefix = f"(履职阶段 note={gate[dep]}*)" if gate[dep] else ""
|
||||
lines.append(f"- `{dep}` 无成功调用记录{prefix}——该上游未被真实使用,本次流程**不得标记为成功**。")
|
||||
for a in artifacts_missing:
|
||||
lines.append(f"- 产物缺失: `{a}`")
|
||||
lines.append("")
|
||||
|
||||
report = "\n".join(lines)
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
out_path.write_text(report + "\n", encoding="utf-8")
|
||||
print(report)
|
||||
print(f"\n[report] {'PASS' if overall else 'FAIL'} → {out_path}", file=sys.stderr)
|
||||
return 0 if overall else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env python3
|
||||
"""le-app-design 证据账本:所有对上游 Skill 的真实调用经此包裹,落入 calls.jsonl。
|
||||
|
||||
用法:
|
||||
执行型调用(CLI 类上游):
|
||||
python3 log_call.py <dep> --run-dir <RUN> -- python3 <DEP>/scripts/search.py "…" --design-system
|
||||
装载型调用(文档型上游 / playbook):
|
||||
python3 log_call.py <dep> --run-dir <RUN> --load <DEP>/SKILL.md
|
||||
|
||||
dep ∈ {ui-ux-pro-max, frontend-design, impeccable, le-app-design(自检)}
|
||||
--run-dir 缺省时按序: $LE_APP_DESIGN_RUN_DIR > <cwd>/le-app-design-runs/<UTC 时间戳>-run/
|
||||
|
||||
行为:
|
||||
- exec: 运行命令,透传 stdout/stderr,透传退出码;成功失败都记账。
|
||||
- load: 读文件记 sha256 与字节数(证明用的是原文而非转述),文件缺失记 FAIL 退出 1。
|
||||
- 记录字段: ts / dep / kind / command / exit_code / duration_ms / stdout_tail /
|
||||
stderr_tail / bytes / sha256 / note。stdout/stderr 仅保留尾部 4000 字符防账本膨胀。
|
||||
|
||||
仅 python3 标准库。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
TAIL = 4000
|
||||
KNOWN_DEPS = {"ui-ux-pro-max", "frontend-design", "impeccable", "le-app-design"}
|
||||
|
||||
|
||||
def default_run_dir() -> Path:
|
||||
env = os.environ.get("LE_APP_DESIGN_RUN_DIR")
|
||||
if env:
|
||||
return Path(env).expanduser()
|
||||
stamp = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%SZ")
|
||||
return Path.cwd() / "le-app-design-runs" / f"{stamp}-run"
|
||||
|
||||
|
||||
def append_record(run_dir: Path, record: dict) -> Path:
|
||||
run_dir.mkdir(parents=True, exist_ok=True)
|
||||
ledger = run_dir / "calls.jsonl"
|
||||
with ledger.open("a", encoding="utf-8") as f:
|
||||
f.write(json.dumps(record, ensure_ascii=False) + "\n")
|
||||
return ledger
|
||||
|
||||
|
||||
def confirm(dep: str, kind: str, status: str, ledger: Path, extra: str = "") -> None:
|
||||
print(
|
||||
f"[log_call] {dep} {kind} {status} → {ledger}"
|
||||
+ (f" ({extra})" if extra else ""),
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
# 手工在第一个裸 "--" 处切分:argparse 的 REMAINDER 会把 -- 之前的选项误吞进命令
|
||||
argv = sys.argv[1:]
|
||||
cmd: list[str] = []
|
||||
if "--" in argv:
|
||||
split = argv.index("--")
|
||||
argv, cmd = argv[:split], argv[split + 1:]
|
||||
|
||||
ap = argparse.ArgumentParser(description="le-app-design 调用账本")
|
||||
ap.add_argument("dep", help="上游标识: ui-ux-pro-max | frontend-design | impeccable | le-app-design")
|
||||
ap.add_argument("--run-dir", default=None, help="运行目录(缺省自动创建)")
|
||||
ap.add_argument("--note", default="", help="阶段/用途备注,如 S2-design-system")
|
||||
ap.add_argument("--load", metavar="PATH", default=None, help="装载型调用:记录读取的原文文件")
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
dep = args.dep
|
||||
if dep not in KNOWN_DEPS:
|
||||
print(f"[log_call] 未知依赖标识 {dep!r};允许: {sorted(KNOWN_DEPS)}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
run_dir = Path(args.run_dir).expanduser() if args.run_dir else default_run_dir()
|
||||
base = {"ts": datetime.now(timezone.utc).isoformat(timespec="seconds"), "dep": dep, "note": args.note}
|
||||
|
||||
if args.load:
|
||||
path = Path(args.load).expanduser()
|
||||
rec = dict(base, kind="load", command=f"load:{path}")
|
||||
try:
|
||||
data = path.read_bytes()
|
||||
except OSError as e:
|
||||
rec.update(exit_code=1, stderr_tail=f"read failed: {e}"[:TAIL])
|
||||
ledger = append_record(run_dir, rec)
|
||||
confirm(dep, "load", "FAIL", ledger, str(e))
|
||||
return 1
|
||||
rec.update(
|
||||
exit_code=0,
|
||||
bytes=len(data),
|
||||
sha256=hashlib.sha256(data).hexdigest(),
|
||||
)
|
||||
ledger = append_record(run_dir, rec)
|
||||
confirm(dep, "load", "OK", ledger, f"{len(data)} bytes sha256:{rec['sha256'][:12]}…")
|
||||
return 0
|
||||
|
||||
if not cmd:
|
||||
print("[log_call] 需要 --load PATH 或 -- <命令…>", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
rec = dict(base, kind="exec", command=json.dumps(cmd, ensure_ascii=False))
|
||||
t0 = time.monotonic()
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
cmd, capture_output=True, text=True, timeout=900,
|
||||
env=dict(os.environ, PYTHONDONTWRITEBYTECODE="1"),
|
||||
)
|
||||
rc, out, err = proc.returncode, proc.stdout or "", proc.stderr or ""
|
||||
except subprocess.TimeoutExpired as e:
|
||||
rc, out, err = 124, (e.stdout or b"").decode(errors="replace") if isinstance(e.stdout, bytes) else (e.stdout or ""), f"timeout after {e.timeout}s"
|
||||
except FileNotFoundError as e:
|
||||
rc, out, err = 127, "", f"command not found: {e}"
|
||||
|
||||
rec.update(
|
||||
exit_code=rc,
|
||||
duration_ms=int((time.monotonic() - t0) * 1000),
|
||||
stdout_tail=out[-TAIL:],
|
||||
stderr_tail=err[-TAIL:],
|
||||
)
|
||||
ledger = append_record(run_dir, rec)
|
||||
if out:
|
||||
sys.stdout.write(out)
|
||||
if not out.endswith("\n"):
|
||||
sys.stdout.write("\n")
|
||||
if err:
|
||||
sys.stderr.write(err)
|
||||
if not err.endswith("\n"):
|
||||
sys.stderr.write("\n")
|
||||
confirm(dep, "exec", f"exit={rc}", ledger, f"{rec['duration_ms']}ms")
|
||||
return rc
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,67 @@
|
||||
# Le SPEC
|
||||
|
||||
> 智能需求分析与 SPEC 生成总控:把模糊、零散、矛盾的想法,经**真实集成的** Grill 深度访谈、Think 方案挑战、OpenSpec 规格工程,转化为可执行、可测试的正式 SPEC。VibeCoding / AI Coding 工作流的需求工程入口。
|
||||
|
||||
**状态**:rigorous 档 | **正式名称 Le SPEC,技术标识 `le-spec`** | 优先适配 Codex,兼容 ZCode / Claude Code
|
||||
|
||||
## 它解决什么问题
|
||||
|
||||
你有一堆没想清楚的想法(甚至互相矛盾),直接丢给 Coding Agent 会得到一堆它自行猜测的「需求」。Le SPEC 在编码之前接管:先访谈把该想清楚的想清楚,再挑战不合理方案,最后产出结构化的 OpenSpec 规格(产品需求 / 技术设计 / 开发任务三分离),让 Coding Agent 不用猜。
|
||||
|
||||
## 架构:真实集成,不是借鉴
|
||||
|
||||
| 引擎 | 来源 | 在 Le SPEC 中的职责 | 调用方式 |
|
||||
|---|---|---|---|
|
||||
| grilling (+ domain-modeling) | [mattpocock/skills](https://github.com/mattpocock/skills) | Phase 2 深度访谈:design tree / frontier 轮次 / 推荐答案 | Skill 工具按名调用,或 Read 其原始 SKILL.md 遵循原义;记调用账本 |
|
||||
| think | [tw93/Waza](https://github.com/tw93/Waza) | Phase 3 方案分析:单一推荐、Simplicity Gate、premise collapse | 同上 |
|
||||
| openspec CLI + skills | [Fission-AI/OpenSpec](https://github.com/Fission-AI/OpenSpec) | Phase 5 规格生成(proposal/specs/design/tasks)、validate、变更与归档 | `openspec` CLI 真实执行(new change / instructions / validate) |
|
||||
|
||||
「真实调用」的证据是**调用账本**(项目内 `openspec/le-spec/ledger.jsonl`):每次依赖交互记录 dep/kind/method/cmd/exit。审查与交付阶段核对账本,无账本条目视为未发生。协议细节见 [references/dependencies.md](references/dependencies.md)。
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
# 1. 依赖(一次性;skills.sh 落 ~/.agents/skills,软链各 runtime)
|
||||
npx skills@latest add mattpocock/skills --skill grilling grill-me grill-with-docs domain-modeling -g -a claude-code codex
|
||||
npx skills@latest add tw93/Waza --skill think -g -a claude-code codex
|
||||
npx skills@latest add Fission-AI/OpenSpec --skill openspec-propose openspec-new-change openspec-update-change openspec-verify-change openspec-archive-change openspec-apply-change openspec-explore -g -a claude-code codex
|
||||
npm install -g @fission-ai/openspec@latest
|
||||
|
||||
# 2. 本 skill(MetaSkill 仓库真源 + 软链)
|
||||
ln -sfn <本仓库路径>/skills/le-spec ~/.agents/skills/le-spec
|
||||
|
||||
# 3. 验证
|
||||
python3 ~/.agents/skills/le-spec/scripts/deps_check.py # required 全绿 exit 0
|
||||
```
|
||||
|
||||
目标项目无需预装 OpenSpec:Phase 5 检测到无根时会征求用户意见再 `openspec init`。
|
||||
|
||||
## 使用
|
||||
|
||||
自然语言直接开始,例如:
|
||||
|
||||
- 「帮我把这个想法整理成 spec:我想做一个……」(零散长文也行,直接贴)
|
||||
- 「先 grill 我一下,把这个功能想透」
|
||||
- 「需求变了,更新之前那个 spec」
|
||||
|
||||
中断随时可恢复:状态存 `<项目>/openspec/le-spec/state.md`,新会话说「继续上次的 le-spec 分析」即可。产出批准后:实现交给 Coding Agent(openspec-apply-change 工作流),需求变更走 openspec-update-change,完成归档 openspec-archive-change。
|
||||
|
||||
## 工作流(逻辑七阶段,按复杂度自适应压缩)
|
||||
|
||||
```
|
||||
0 识别分级 → 1 需求结构化(十类分拣) → 2 Grill 访谈(动态轮次·每4轮检查点·收敛即停)
|
||||
→ 3 Think 挑战(有分歧才进) → 4 反向验收(独立工程师视角找"还得猜什么")
|
||||
→ 5 OpenSpec 生成(validate 通过) → 6 四视角独立审查 → 7 用户批准(硬停点)
|
||||
```
|
||||
|
||||
详见 [SKILL.md](SKILL.md) 与 [references/](references/)。
|
||||
|
||||
## 安全边界
|
||||
|
||||
- 未经用户明确批准不进入实现(Phase 7 回合终止硬停点)。
|
||||
- required 依赖缺失 → 停止并给安装命令,不以模拟调用冒充。
|
||||
- 不编造业务规则/验收标准;AI 推荐方案标 [建议],不冒充已批准。
|
||||
|
||||
## 已知限制
|
||||
|
||||
见 [reports/test-report.md](reports/test-report.md)(测试 A-I 执行记录与未验证项)。
|
||||
@@ -0,0 +1,70 @@
|
||||
---
|
||||
name: le-spec
|
||||
description: |
|
||||
Le SPEC —— 智能需求分析与 SPEC 生成总控:把模糊、零散、矛盾的自然语言想法,经真实集成的 Grill 深度访谈、Think 方案挑战、OpenSpec 规格工程,转化为可执行、可测试的正式 SPEC。
|
||||
何时触发:用户想从想法或零散需求出发,先梳理清楚再开发("帮我把这个想法整理成 spec""我要做个 App,先分析下需求""这些需求有点乱,帮我梳理""先想清楚再写代码""来一场需求访谈""write a spec""refine these requirements");提到 grill / openspec / 需求工程 / SPEC 生成;已有项目新增功能或变更需求要走规格("给这个功能写个 spec""需求变了,更新规格")。
|
||||
不要触发:已批准 SPEC 的实现与编码(交给 Coding Agent / openspec-apply-change);纯 PRD/需求文档的润色与排版;与需求澄清无关的普通文档写作;用户明说"别做 spec、直接写代码、直接改代码"。
|
||||
metadata:
|
||||
author: Leroy
|
||||
version: "1.0.0"
|
||||
runtimes: zcode, claude-code, codex
|
||||
requires: grilling + grill-me/grill-with-docs + domain-modeling (mattpocock/skills), think (tw93/Waza), openspec CLI + openspec-* skills (Fission-AI/OpenSpec)
|
||||
---
|
||||
|
||||
# Le SPEC —— 需求分析与 SPEC 生成总控
|
||||
|
||||
用户负责表达意图与做关键决策;你负责理解、挖掘、思考、推荐、整理与验证。
|
||||
产出物不是"更漂亮的需求文档",而是**在开始编码前把该想清楚的都想清楚**的正式 SPEC。
|
||||
|
||||
## 三条铁律(先于一切流程)
|
||||
|
||||
1. **真实调用,不冒充。** 访谈必须真实加载 `grilling` 原文并遵循其 design-tree/frontier 机制;方案分析必须真实加载 `think` 原文;规格必须经 `openspec` CLI 生成与校验。禁止用自拟提示词模拟依赖行为。每次依赖交互记入调用账本(见下),无账本条目视为未发生。
|
||||
2. **不重复、不复盘。** 已确认的决策写入状态文件后,后续任何阶段(Think / 验收 / 审查)只消费不重问;发现新不确定性时做**针对性**追问,不重启流程。
|
||||
3. **批准前不编码。** Phase 7 是回合终止硬停点:向用户呈交确认包后终止回合,未经用户明确批准,不得创建/修改任何实现代码,不得执行 openspec-apply-change。
|
||||
|
||||
## 阶段机
|
||||
|
||||
| 阶段 | 做什么 | 依赖调用 | 退出条件 |
|
||||
|---|---|---|---|
|
||||
| 0 识别 | 判项目类型、只读检查、复杂度分级 | `openspec context --json`(探测已有根) | 复杂度定档、上下文清单成形 |
|
||||
| 1 理解 | 原始需求结构化(十类分拣) | — | `state.md` 初始化完成 |
|
||||
| 2 访谈 | Grill 深度访谈 | **grilling**;已有代码库时叠加 **domain-modeling** | 收敛标准满足(见 workflow.md) |
|
||||
| 3 方案 | 方案分析与挑战 | **think**(无实质分歧可跳过) | 推荐方案获用户确认 |
|
||||
| 4 验收 | 需求反向验收(独立工程师视角) | 独立子代理(fresh context) | 无 blocking 级未决问题 |
|
||||
| 5 规格 | OpenSpec 生成正式 SPEC | **openspec CLI** + openspec-propose/new-change 工作流 | artifacts 齐全且 `validate` 通过 |
|
||||
| 6 审查 | SPEC 独立质量审查(四视角) | 独立子代理 + `openspec validate` | 审查问题清零或降为非阻塞 |
|
||||
| 7 交接 | 用户确认与交接 | — | 用户明确批准 → 交给 Coding Agent |
|
||||
|
||||
- 每阶段详细规程:[workflow.md](references/workflow.md)
|
||||
- 依赖真实调用协议:[dependencies.md](references/dependencies.md)(**Phase 2/3/5 入口必读**)
|
||||
- Phase 4 规程:[acceptance.md](references/acceptance.md);Phase 6 规程:[review.md](references/review.md)
|
||||
|
||||
阶段是逻辑流不是机械串行:simple 档压缩执行(见 workflow.md 复杂度分档),用户可随时切换讨论模块、修改需求、查看草稿——但**只能向前跳过 AI 侧环节,不能跳过用户确认停点**。
|
||||
|
||||
## 启动协议(每次触发本 skill 时)
|
||||
|
||||
1. 定位项目 OpenSpec 根:`openspec context --json`。有根且存在 `<root>/openspec/le-spec/state.md` → 读状态,从其「下一步」恢复,不重做已完成阶段。
|
||||
2. 无状态文件 → 全新分析:跑 `python3 <skill_dir>/scripts/deps_check.py` 检查依赖(见铁律 1)。缺失 required 级依赖 → 停止并给出精确安装命令(见 dependencies.md),不得以降级模拟继续。
|
||||
3. 按 Phase 0 → 1 进入。
|
||||
|
||||
## 状态与账本(持久化,跨会话恢复)
|
||||
|
||||
- `<root>/openspec/le-spec/state.md` —— 阶段、已确认决策、否定方案、开放问题、假设、下一步、访谈轮次计数。每轮重要交互后增量更新;用户变更需求时记录影响范围。
|
||||
- `<root>/openspec/le-spec/ledger.jsonl` —— 调用账本,经 `scripts/ledger.py append` 记录每次依赖交互(dep / kind / cmd / exit / summary)。Phase 7 交付与 Phase 6 审查以账本为真实调用证据。
|
||||
- 项目无 OpenSpec 根时暂用 `.le-spec/` 存放,待 Phase 5 init 后迁移。
|
||||
|
||||
## 停手点(回合终止硬约束)
|
||||
|
||||
1. 依赖检查失败 → 停,给安装指引。
|
||||
2. Phase 2 每轮问完 → 终止回合等用户回答;Phase 3 推荐方案给出 → 等用户决断。
|
||||
3. Phase 4 发现 blocking 问题 → 终止回合,向用户澄清后才能进 Phase 5。
|
||||
4. Phase 7 呈交确认包 → **终止回合**;未获明确批准不得动实现代码。
|
||||
5. 用户要求删除/重写已批准 SPEC 的大范围内容 → 停,先确认影响范围。
|
||||
|
||||
## 反例黑名单
|
||||
|
||||
- 不把临时想法当已确认需求,不把 AI 推荐方案当用户已批准。
|
||||
- 不为凑轮数追问,也不为省事跳过 blocking 问题。
|
||||
- 不在 SPEC 里编造业务规则、验收标准或技术约束。
|
||||
- 不把产品需求、技术设计、开发任务混写进一个文件(OpenSpec 三 artifact 天然分离,遵循它)。
|
||||
- 不在旧会话假设新装依赖"应该可用"——调用前先 deps_check 或确认账本路径可写。
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"_说明": "Le SPEC 触发用例。应触发:从想法/零散需求出发要梳理成规格;不应触发:已批准 SPEC 的实现、纯文案、无关任务。family 标注场景族。",
|
||||
"recommended_threshold": 0.3,
|
||||
"negative_patterns": ["直接写代码", "直接改代码", "别做 spec", "别做访谈", "润色", "排版", "只要总结", "翻译一下", "帮我实现"],
|
||||
"should_trigger": [
|
||||
{"text": "帮我把这个想法整理成 spec,我想做一个记账 App", "family": "idea_to_spec"},
|
||||
{"text": "这些需求有点乱,你先帮我梳理梳理,想清楚了再动手", "family": "messy_requirements"},
|
||||
{"text": "我要做个 App,先分析下需求,来一场深度访谈", "family": "explicit_interview"},
|
||||
{"text": "用 openspec 给这个新功能写个正式的规格", "family": "openspec_named"},
|
||||
{"text": "先 grill 我一下,把这个方案想透再写规格", "family": "grill_named"},
|
||||
{"text": "需求变了,把之前批准的 spec 更新一下,加一个导出功能", "family": "spec_change"},
|
||||
{"text": "refine these rough requirements into a testable spec", "family": "en_trigger"}
|
||||
],
|
||||
"should_not_trigger": [
|
||||
{"text": "spec 已经批了,现在开始实现 tasks 里的任务吧", "family": "implementation_phase"},
|
||||
{"text": "帮我把这份 PRD 润色一下排版更好看", "family": "doc_polish"},
|
||||
{"text": "这段代码有个 bug,帮我修一下", "family": "unrelated_task"},
|
||||
{"text": "别做 spec 了直接改代码,改完告诉我就行", "family": "explicit_skip"}
|
||||
],
|
||||
"near_neighbor": [
|
||||
{"text": "帮我把这个想法整理成 skill", "family": "meta_confusion_should_route_le_meta_skill"}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
# Phase 4:需求反向验收规程
|
||||
|
||||
**问题意识**:Phase 4 检查的不是"文档漂不漂亮",而是**需求是否明确到足以形成规格**。核心一问:
|
||||
|
||||
> "一个完全没有参与讨论的资深工程师现在接手开发,哪些重要产品行为仍然需要他自己猜测?哪些必须回头找需求负责人确认?"
|
||||
|
||||
## 执行方式(按优先级)
|
||||
|
||||
1. **子代理独立审查(首选)**:用 Agent 工具派 fresh-context 子代理(general-purpose 或 Explore)。提示词要点:
|
||||
- 角色:资深开发工程师,第一次见到这个项目;
|
||||
- 输入:只给 state.md 路径(+ 已有 openspec specs 路径,如有)——**不给会话历史、不给 Le SPEC 的阶段叙事**,确保视角独立;
|
||||
- 任务:逐条检查下列清单,输出"我会被迫猜测/必须确认"的清单,每条注明影响哪个功能点与后果严重度;
|
||||
- 产出格式:问题列表(问题/影响/严重度建议/建议澄清方式)。
|
||||
2. **无子代理能力时的降级**:自审前显式重置角色("放下前文立场,作为首次接手的工程师仅凭 state.md 判断"),只允许引用 state.md 与项目文件内容。**必须在产出中声明"本轮为同上下文自审,独立性弱于子代理审查"**——不装作独立。
|
||||
|
||||
## 检查清单
|
||||
|
||||
- [ ] 用户流程有断点(走不到下一步/回不去)
|
||||
- [ ] 功能之间缺关联规则(A 发生时 B 如何)
|
||||
- [ ] 数据关系歧义(谁拥有谁、一对多还是多对多、删除级联)
|
||||
- [ ] 状态机不完整(缺状态、缺转换、缺终态)
|
||||
- [ ] 权限与业务边界不明(谁能做什么、越界怎样)
|
||||
- [ ] 异常场景缺处理原则(失败/超时/并发/空数据)
|
||||
- [ ] 需求间冲突(同一对象两种定义)
|
||||
- [ ] 未经确认的重要默认假设(state.md 里的 [默认] 项逐条复核用户知情度)
|
||||
- [ ] 验收判据缺失(做完了怎么算"对")
|
||||
|
||||
## 分级与处置
|
||||
|
||||
| 级别 | 判定 | 处置 |
|
||||
|---|---|---|
|
||||
| blocking | 不澄清就无法实现核心行为 / 两种理解导致完全不同的产品 | 终止回合向用户澄清(用 grilling 单轮格式,记账本) |
|
||||
| major | 影响正确性但可从已确认决策推导 | 推导入 state.md 并标注依据;推导不了升 blocking |
|
||||
| minor | 边缘场景、可延后 | 记开放问题,进 SPEC 的"非阻塞事项" |
|
||||
|
||||
更新后**只复核受影响项**,不重跑全清单。清零 blocking → 进 Phase 5。
|
||||
@@ -0,0 +1,100 @@
|
||||
# 依赖真实调用协议
|
||||
|
||||
Le SPEC 的价值建立在三个外部能力**真实运行**之上。本文件定义每个依赖的本体位置、调用条件、调用方式、上下文交接与失败处理。
|
||||
|
||||
## 0. 调用方式总则(skill 类依赖)
|
||||
|
||||
所有 skill 类依赖(grilling / domain-modeling / think / openspec-*)按以下顺序选择调用方式:
|
||||
|
||||
1. **宿主 Skill 工具**:若当前 runtime 提供 Skill 工具且该依赖已在可用列表中,用 Skill 工具按名调用。
|
||||
2. **文件直读**:否则用 Read 读取依赖原始 `SKILL.md` **全文**,把其内容作为当前回合的执行指令遵循。这不是降级——skill 的本质就是指令文档,宿主 Skill 工具内部做的也是加载原文;直读与工具调用在语义上等价,前提是**读原文、遵循原义**(包括其引用的 references 子文件,如 think 的 mode-evaluation.md)。
|
||||
|
||||
两种方式都必须:
|
||||
- 先记入调用账本(`ledger.py append --dep <name> --kind skill-load --method skill-tool|file-read`);
|
||||
- 遵循原文机制,不自拟替代提示词(例如 grilling 的 frontier 轮次格式 ❓Q/➡️推荐、think 的 Simplicity Gate、openspec-propose 的 artifact 循环)。
|
||||
|
||||
## 1. grilling —— 访谈引擎
|
||||
|
||||
| 项 | 值 |
|
||||
|---|---|
|
||||
| 来源 | github.com/mattpocock/skills(productivity/grilling) |
|
||||
| 本体 | `~/.agents/skills/grilling/SKILL.md`(软链解析后读真源) |
|
||||
| 入口语义 | `grill-me` = 只调 grilling;`grill-with-docs` = grilling + domain-modeling。Le SPEC 按此原义路由:**全新项目 → grilling;已有代码/文档 → grilling + domain-modeling** |
|
||||
| 调用时机 | Phase 2 全程(simple 档也走它,只是轮数少) |
|
||||
| 输入交接 | state.md 的「已确认决策」「开放问题」两节——已确认项不得再问;开放问题构成 design tree 的初始节点 |
|
||||
| 原生机制(必须保留) | design tree、frontier(前置决策已定的可问集合)、每轮问整个 frontier 并给推荐答案、答案重塑树、"事实自己查(可派子代理)、决策问用户"、frontier 空且用户确认共识才算完 |
|
||||
| 产物 | 每轮问题(❓Q1… ➡️推荐格式);用户回答由 Le SPEC 写入 state.md |
|
||||
|
||||
**Le SPEC 叠加规则**(不与原义冲突):
|
||||
- 每轮用户回答后:决策入 state.md,轮次 +1;每约 4 轮做一次检查点(见 workflow.md Phase 2)。
|
||||
- grilling 的"会话结束条件 = frontier 空"之外,Le SPEC 用需求收敛标准(workflow.md)做结束判定;两者取先到者,且需向用户确认共识后收口。
|
||||
- 已有项目时叠加 domain-modeling:访谈中发现术语模糊 → 按其"Sharpen fuzzy language"当场澄清;硬决策按其三条件(难逆转/无上下文会困惑/真实取舍)写 ADR。GLOSSARY.md/ADR 由 domain-modeling 机制管理,Le SPEC 不另建。
|
||||
|
||||
## 2. think —— 方案分析引擎
|
||||
|
||||
| 项 | 值 |
|
||||
|---|---|
|
||||
| 来源 | github.com/tw93/Waza(skills/think) |
|
||||
| 本体 | `~/.agents/skills/think/SKILL.md` |
|
||||
| 调用时机 | Phase 3:出现重大产品分歧、技术选型、架构取舍、过度设计疑虑、值不值得判断。**simple 档且无实质争议 → 跳过并注明** |
|
||||
| 模式选择(遵循其原生定义) | 定义清楚的修复计划 → Lightweight Mode;价值/存废判断 → 读 references/mode-evaluation.md;批量诉求 → mode-triage.md。默认全模式 |
|
||||
| 输入交接 | state.md 已确认需求 + 约束 + 已否定方案 + 待选方案。think 的 "Durable Context Preflight" 要求读项目 AGENTS.md/CLAUDE.md 索引——照做 |
|
||||
| 原生机制(必须保留) | 先查官方方案、推荐单一方案+理由+一个替代、premise collapse(最脆弱假设)、attack angles(外部依赖/并发/迁移时)、Simplicity Gate、批准前不动代码 |
|
||||
| 产物 | 推荐方案 + 关键决策 + 风险 + 被否替代 → 写入 state.md「已确认决策」(待用户批准的标 [待批准]) |
|
||||
|
||||
**回流规则**:think 发现新的重大需求不确定性(Blocking ambiguities)→ 带回 Phase 2 用 grilling 格式做**针对性**追问(不是重启访谈),解决后回到 Phase 3 继续。
|
||||
|
||||
## 3. OpenSpec —— 规格工程引擎(CLI + skills 双通道)
|
||||
|
||||
| 项 | 值 |
|
||||
|---|---|
|
||||
| 来源 | github.com/Fission-AI/OpenSpec(npm: @fission-ai/openspec) |
|
||||
| CLI | `openspec`(PATH 中,≥1.3 可用 artifact 工作流;本 skill 按 1.16.x 验证) |
|
||||
| skills | `~/.agents/skills/openspec-{propose,new-change,update-change,verify-change,archive-change,apply-change,explore}/SKILL.md` |
|
||||
| 调用时机 | Phase 0(context 探测)、Phase 5(生成)、Phase 6(validate)、Phase 7 后的变更与归档 |
|
||||
|
||||
**Phase 5 标准路径**(遵循 openspec-propose 原生步骤,Le SPEC 已备齐输入故跳过其澄清步):
|
||||
|
||||
```bash
|
||||
openspec context --json # 定位根;root:null → 见下"项目初始化"
|
||||
openspec new change "<kebab-name>" # 脚手架(默认 spec-driven schema)
|
||||
openspec status --change "<name>" --json # artifact 顺序与路径
|
||||
openspec instructions <artifact> --change "<name>" --json # 逐个:proposal → specs → design → tasks
|
||||
# 按 template + instruction 撰写每个 artifact 文件
|
||||
openspec validate "<name>" # 必须通过
|
||||
```
|
||||
|
||||
- **artifact 分工是 OpenSpec 原生的**:`specs/` = 产品需求(行为契约、Requirement/Scenario)、`design.md` = 技术设计(HOW,conditional)、`tasks.md` = 开发任务(checkbox)、`proposal.md` = Why/What/Capabilities。不混写。
|
||||
- **项目初始化边界**(遵循其 Project check 原义):`root:null` 且用户未提 OpenSpec → 停下问用户:init(`openspec init --tools none`,工具注入由用户后续决定)/ 走 store / 不用 OpenSpec 本次降级输出。绝不让 `new change` 的副作用悄悄创建根。
|
||||
- **变更管理**:SPEC 已批准后的需求变化 → openspec-update-change 工作流(新 change 或修订现有),归档 → openspec-archive-change。Le SPEC 交接到此为止,apply 交给 Coding Agent。
|
||||
|
||||
## 4. 依赖检查与失败处理
|
||||
|
||||
`scripts/deps_check.py`(--json 供脚本、默认人读输出):
|
||||
|
||||
| 依赖 | 级别 | 缺失时 |
|
||||
|---|---|---|
|
||||
| grilling skill | required | 停止访谈阶段,输出安装命令(下表),不降级为普通问答 |
|
||||
| think skill | required(Phase 3 入口) | 方案分歧时停止并提示安装;无分歧场景不受影响 |
|
||||
| openspec CLI | required(Phase 5 入口) | 停止规格生成,提示 `npm install -g @fission-ai/openspec` |
|
||||
| openspec-* skills | recommended | CLI 工作流仍可走(Phase 5 标准路径即 CLI 驱动),但提示安装以获得完整工作流指令 |
|
||||
| domain-modeling skill | recommended(已有项目时升 required) | 已有项目的访谈照常,但声明"术语沉淀与 ADR 不可用" |
|
||||
| grill-me / grill-with-docs | optional(入口薄壳) | 不影响——Le SPEC 直接路由到底层组合 |
|
||||
|
||||
安装命令(真实可执行,2026-11 验证):
|
||||
|
||||
```bash
|
||||
npx skills@latest add mattpocock/skills --skill grilling grill-me grill-with-docs domain-modeling -g
|
||||
npx skills@latest add tw93/Waza --skill think -g
|
||||
npx skills@latest add Fission-AI/OpenSpec --skill openspec-propose openspec-new-change openspec-update-change openspec-verify-change openspec-archive-change openspec-apply-change openspec-explore -g
|
||||
npm install -g @fission-ai/openspec@latest
|
||||
```
|
||||
|
||||
**失败语义**:CLI 非零退出 ≠ 永远失败(如 `openspec list` 在无根项目返回非零,那是"未初始化"的答案不是崩溃)——读 JSON 输出判断,不盲目重试。skill 加载失败(文件不存在/为空)→ 记账本(method=file-read, exit=1)→ 按缺失处理。**任何情况下不得用模拟输出冒充调用成功。**
|
||||
|
||||
## 5. 防重复机制
|
||||
|
||||
- grilling 问过的 → state.md「已确认决策」,think 直接消费;
|
||||
- think 已否定的方案 → state.md「已否定方案」,Phase 4/6 审查不再重提,除非发现新证据;
|
||||
- OpenSpec artifacts 已写内容 → Phase 6 审查对照 state.md 检查一致性,不重新发起需求讨论;
|
||||
- 每阶段入口先读 state.md 的阶段标记,已完成阶段不重做(中断恢复同理)。
|
||||
@@ -0,0 +1,35 @@
|
||||
# Phase 6:SPEC 独立质量审查规程
|
||||
|
||||
**问题意识**:Phase 4 检查"需求是否明确到足以形成规格";Phase 6 检查**"规格是否准确到足以指导开发和测试"**。审查对象是 OpenSpec artifacts(proposal/specs/design/tasks),不是重新审需求——需求问题应已在 Phase 4 清零。
|
||||
|
||||
## 审查组成(三路并行)
|
||||
|
||||
1. **四视角子代理审查**(首选 fresh-context 子代理;降级规则同 acceptance.md,须声明独立性等级)。输入:change 目录全部 artifact 路径 + state.md 路径 + (已有项目时)相关既有 specs 路径。四视角各答各的:
|
||||
|
||||
| 视角 | 追问 |
|
||||
|---|---|
|
||||
| 产品 | 完整表达真实需求了吗?有无遗漏功能?有无混入用户未确认的新要求(对照 state.md 逐条溯源)? |
|
||||
| 架构 | 与现有系统/既有 specs 冲突?重大技术风险?不必要的复杂度(对照 design.md 的 Decisions 与 think 结论)? |
|
||||
| 开发 | 还要猜什么?缺数据/流程/状态定义?互相矛盾的要求(specs 内部 + specs↔design↔tasks)? |
|
||||
| 测试 | 每条 Requirement 的 Scenario 可执行可判定的吗?正常/异常/边界覆盖?有没有判不了 pass/fail 的模糊表述? |
|
||||
|
||||
2. **CLI 校验**:`openspec validate "<change>"`(严格模式可用则用)+ `openspec status --change "<name>"` 确认 artifact 齐全。
|
||||
3. **账本核对**:`ledger.py check` 确认 grilling/think/openspec 均有真实调用记录;缺失 → 回补(真实调用)或如实声明。
|
||||
|
||||
## 产出格式(每问题一行,不许无依据打分)
|
||||
|
||||
```markdown
|
||||
| # | 视角 | 严重度 | 问题(引 artifact 行文) | 影响 | 修正建议 | 阻塞? |
|
||||
```
|
||||
|
||||
严重度:blocker(规格错误/矛盾/无法实现或测试)|major(会导致误解或返工)|minor(措辞与边缘)。
|
||||
|
||||
## 修正循环
|
||||
|
||||
blocker/major 逐条修正 artifact → 只复验受影响项(重跑 validate + 相关视角复查)→ 清零或全部降为 minor 后出结论:
|
||||
|
||||
```markdown
|
||||
审查结论:blocker 0 / major 0 / minor N(列表,随 SPEC 一并呈交)
|
||||
```
|
||||
|
||||
修正时保持铁律 2:修正只动规格文本与事实呈现,不重启需求讨论;发现规格确实承载不了的需求歧义 → 回 Phase 4 流程处理。
|
||||
@@ -0,0 +1,106 @@
|
||||
# 七阶段工作流规程
|
||||
|
||||
阶段是逻辑流。每阶段入口先读 state.md 阶段标记,已完成的跳过。
|
||||
|
||||
## Phase 0:项目识别与上下文理解
|
||||
|
||||
1. 判任务类型:全新产品 / 已有产品加功能 / 已有功能修改 / 复杂业务模块 / 架构重构 / 探索性想法。
|
||||
2. 有代码库时只读检查(按复杂度控制深度,simple 档可缩到 3-5 分钟扫读):
|
||||
- 项目结构、技术栈、入口文件;已有文档与 `openspec/specs/`(`openspec context --json` 探测);GLOSSARY/ADR(domain-modeling 的管理物);近期 git log 的设计决策线索。
|
||||
- **能从代码和文档确认的事实,绝不进访谈问题**(grilling 原义:事实自己查,决策问用户)。
|
||||
3. 复杂度定档(可随讨论动态升降级,升降级写入 state.md 并注明理由):
|
||||
|
||||
| 档 | 特征 | 流程 |
|
||||
|---|---|---|
|
||||
| simple | 字段/筛选/文案级修改,影响单一界面或接口 | 0 → 1 → grilling 单轮(≤1 轮)→ (跳过 3)→ 清单式 4 → 5(propose,design 多为 skip)→ 快速 6 → 7 |
|
||||
| medium | 新功能模块(日历、用户管理级) | 全流程,3 按需 |
|
||||
| complex | 全新产品、多角色/多流程系统、架构重构 | 全流程;3 必用于重大决策;访谈允许多轮充分展开 |
|
||||
|
||||
## Phase 1:原始需求理解
|
||||
|
||||
对用户原始输入做**十类分拣**,产出写入 state.md:
|
||||
|
||||
1. 明确需求 2. 目标与期望结果 3. 用户角色与场景 4. 未定想法 5. 用户假设 6. 约束(产品+技术) 7. 矛盾描述(列出冲突双方与出处) 8. 重要缺失信息 9. 已否定方案 10. 明确不做
|
||||
|
||||
规则:保留原始措辞的关键短语与来源(哪条消息);不擅自增需求;临时想法标注 [未定];AI 推荐方案标注 [建议],不得记为已批准。矛盾不在此阶段裁决——入开放问题,Phase 2 问。
|
||||
|
||||
state.md 初始化模板:
|
||||
|
||||
```markdown
|
||||
# Le SPEC 状态 — <项目/功能名>
|
||||
更新: <日期> | 阶段: Phase 1 | 复杂度: <档>(理由)
|
||||
|
||||
## 项目
|
||||
类型: <全新/加功能/修改/…> | 目标: <一句话>
|
||||
|
||||
## 原始输入摘要
|
||||
- (来源: 首条消息) <要点,保留关键原话>
|
||||
|
||||
## 已确认决策
|
||||
| # | 决策 | 内容 | 来源 | 状态 |
|
||||
|---|------|------|------|------|
|
||||
|
||||
## 已否定方案
|
||||
- <方案> — 否定原因 <来源>
|
||||
|
||||
## 开放问题
|
||||
- [ ] Q1 <问题>(影响: 核心/边缘;依赖: -)
|
||||
|
||||
## 假设(待验证)
|
||||
- <假设>(依据)
|
||||
|
||||
## 访谈计数
|
||||
轮次: 0 | 检查点: -
|
||||
|
||||
## 下一步
|
||||
- <具体动作>
|
||||
```
|
||||
|
||||
## Phase 2:Grill 深度访谈
|
||||
|
||||
入口动作(顺序固定):
|
||||
1. 读 [dependencies.md](dependencies.md) 第 1 节,按调用方式总则**真实加载 grilling**(已有项目叠加 domain-modeling),记账本。
|
||||
2. 以 state.md 开放问题 + 十类分拣中的矛盾/缺失为 design tree 初始节点。
|
||||
|
||||
访谈中 Le SPEC 叠加于 grilling 原义之上的规则:
|
||||
- **每轮结束**(用户答完):新决策入表、开放问题更新、轮次 +1、增量写 state.md。
|
||||
- **检查点**:每逢第 4、8、12…轮答完,附一段 ≤6 行小结(已确认 N 项 / 开放 M 项 / 收敛判断:继续|接近|已达标)。有 blocking 问题默认继续,不要求用户批准继续;已达标主动提议收口。
|
||||
- **收敛标准**(全部满足才收口):目标明确 | 范围明确(含不做) | 角色明确 | 核心流程闭环 | 业务规则明确 | 数据关系与权限边界明确(适用时) | 异常边界已有处理原则 | 无阻塞实现的重大未决。非关键细节可用「明确标注的默认方案」填充(标注 [默认] 进 state.md),不无限追问。
|
||||
- **用户控制权**:随时可换模块、改需求(记录影响范围,受影响的已确认项重开并注明)、看草稿、暂停(state.md 存「下一步」)、先谈技术(跳 Phase 3 再回)。
|
||||
- 关键需求仍有歧义时不得自行拍板;重大未决可出「标注风险的草稿」但明示未经审查。
|
||||
|
||||
## Phase 3:Think 方案分析与挑战
|
||||
|
||||
1. 无实质方案争议(simple 或方案唯一且用户已定)→ state.md 记"Phase 3 跳过(无分歧)",进 Phase 4。
|
||||
2. 否则按 [dependencies.md](dependencies.md) 第 2 节**真实加载 think**(含其 Preflight:读项目 AGENTS/CLAUDE 索引与相关规则),记账本。
|
||||
3. think 产出推荐方案 → 向用户呈现(推荐+理由+一个替代+风险+最脆弱假设)→ **终止回合等决断** → 批准则入 state.md [已批准],新不确定性回流 Phase 2。
|
||||
|
||||
## Phase 4:需求反向验收
|
||||
|
||||
详见 [acceptance.md](acceptance.md)。核心:fresh-context 子代理扮演"没参与讨论的资深工程师",只读 state.md + 已确认材料,回答"由我实现的话,哪些产品行为还得靠猜?"。产出 blocking / major / minor 问题清单:
|
||||
- blocking → 以 grilling 单轮格式向用户澄清(记账本 dep=grilling, summary=acceptance-driven)→ 更新后**只复核受影响项**;
|
||||
- major → 能由已确认决策推导的当场补进 state.md 并标注推导依据,推导不了的升级 blocking;
|
||||
- minor → 记入开放问题,随 SPEC 的"非阻塞事项"呈现。
|
||||
清零 blocking 才进 Phase 5。
|
||||
|
||||
## Phase 5:OpenSpec 生成正式 SPEC
|
||||
|
||||
按 [dependencies.md](dependencies.md) 第 3 节标准路径执行(每条 CLI 命令记账本)。要点:
|
||||
- change 名 kebab-case,从需求导出;同名已存在 → 问用户续用还是新建(遵循原生 guardrail)。
|
||||
- 逐 artifact:先 `instructions`,按其 template + instruction 撰写;内容全部来自 state.md(已批准决策/已确认需求/[默认] 项如实标注),**不编造规则**;验收场景用 Requirement/Scenario 结构,可测、可观察。
|
||||
- `design.md` 遵循其 conditional 规则(跨切面/新依赖/数据模型等才写);Phase 3 有 think 结论的,方案与已否替代直接沉淀为 Decisions。
|
||||
- `openspec validate` 必须通过;失败按输出修正后重跑。
|
||||
- SPEC 语言聚焦"系统做什么、表现什么行为、如何判断正确"——实现机制进 design/tasks(OpenSpec config 的 product-language 约定同理)。
|
||||
|
||||
## Phase 6:SPEC 独立质量审查
|
||||
|
||||
详见 [review.md](review.md)。四视角(产品/架构/开发/测试)独立审查 artifacts + `openspec validate` 复跑 + 账本核对。问题分级(blocker/major/minor)→ 修正 → 只复验受影响项 → 出审查结论(无事实依据的分数不打)。
|
||||
|
||||
## Phase 7:用户确认与交接
|
||||
|
||||
确认包含:正式 SPEC 位置与摘要、核心功能与范围、关键决策(含被否方案)、明确不做、尚存风险与非阻塞问题、审查结果摘要、账本统计(grilling/think/openspec 各 N 次真实调用)。
|
||||
**呈交后终止回合。** 用户批准 → 更新 state.md 阶段为"已交接",提示后续:实现用 openspec-apply-change 工作流、需求变化用 openspec-update-change、完成后 openspec-archive-change。用户要改 → 只更新受影响 artifact 并复验(不重跑全流程)。
|
||||
|
||||
## 中断恢复
|
||||
|
||||
任何会话重入:`openspec context --json` → 读 `<root>/openspec/le-spec/state.md` → 校验其「下一步」与 OpenSpec 实际状态(`openspec status --change` 或 `openspec list --json`)一致 → 从断点继续。state.md 阶段标记与实际不符时以实际产物为准并修正状态。
|
||||
@@ -0,0 +1,6 @@
|
||||
# reports/ —— 证据政策
|
||||
|
||||
- `test-report.md` —— 测试 A-I 的执行记录、修正记录与已知限制(**入库**:本次交付要求的可复查证据)
|
||||
- `trigger-eval.json` —— trigger_eval.py 生成物(**不入库**,gitignore:按需重跑即得,遵循仓库先例)
|
||||
|
||||
诚实条款:验证不了的写「证据缺失」,不编造;计划中的工作不是证据。测试脚手架项目(/tmp/lespec-test/)不入库,证据以本报告 + 文中账本数字为指针。
|
||||
@@ -0,0 +1,40 @@
|
||||
# Le SPEC 测试报告(v1.0.0,2026-11-08)
|
||||
|
||||
测试方法:以 fresh-context 子代理为 Le SPEC 执行者、主会话扮演真实用户的多轮对话实测(非单发提示词测试),产物全部可复核。证据分级:[已验证] 有执行记录/文件/账本;[设计优势] 源码可见;[假设] 合理未证。
|
||||
|
||||
## 总览
|
||||
|
||||
| 测试 | 验证点 | 结果 | 关键证据 |
|
||||
|---|---|---|---|
|
||||
| A 零散需求理解 | 混乱输入十类分拣 | ✅ [已验证] | app-idea state.md:矛盾 A(安卓↔iCloud)入开放问题、语音标[未定]、「先不管」搁置不重问 |
|
||||
| B 全新产品+Grill | grilling 真实加载+frontier 机制 | ✅ [已验证] | 账本 grilling×6(file-read);❓Q/➡️推荐格式、每轮问整 frontier、答案重塑树(Q1-C 解锁 R2) |
|
||||
| C 已有项目 | 读代码、grill-with-docs、不重复问 | ✅ [已验证] | todo-app:README/代码被读(29 行/数据路径/无账号),零代码可答问题;GLOSSARY 6 术语(domain-modeling×1 账本) |
|
||||
| D 不合理方案 | think 真实调用+有效挑战 | ✅ [已验证] | 账本 think×2;🥷 前缀/官方方案核查(真实链接)/minimal 选项/premise collapse/被否替代;Redis 被识别为唯一过度点,账号/排序/审计被否 |
|
||||
| E 深度访谈 | >4 轮、每 4 轮检查点、收敛即停 | ✅ [已验证] | 4 轮(变更延长第 4 轮);R4 后检查点(≤6 行/8/8 收敛/frontier 空);收敛后主动收口不凑轮 |
|
||||
| F 反向验收 | 独立视角挖隐藏歧义 | ✅ [已验证] | 子代理场景:D25-D27 推导(编辑规则/清单快照/独立性);主会话独立子代理补测:抓到 2 blocking(用量数据模型、Mac 交付范围)+4 major——**独立审查显著强于自审** |
|
||||
| G SPEC 生成与审查 | openspec 真实工作流 | ✅ [已验证] | 两项目 validate --strict 复跑通过;场景1:6 capability/21 Requirement/46 Scenario;场景2:3 capability/17 tasks;WHEN/THEN 可测场景 |
|
||||
| H 变更与中断恢复 | 状态存取/局部更新/不重复问 | ✅ [已验证] | R3 变更(+采购清单/−微信)→ 变更日志+D1/D4/D9 影响标注;新会话恢复:21 项决策未重问、断点续问 |
|
||||
| I 依赖真实性 | 真实运行/缺失提示/无模拟 | ✅ [已验证] | deps_check 缺失场景 exit 1+精确安装命令;账本共 28 条真实调用(grilling 9/think 2/openspec CLI 14/propose 2/domain-modeling 1);CLI exit 1 被正确解读为「未初始化」非崩溃 |
|
||||
|
||||
测试项目(脚手架,不入库):/tmp/lespec-test/{app-idea,todo-app}。
|
||||
|
||||
## 修正记录(测试驱动)
|
||||
|
||||
1. **触发词面缺口**:初版 description 对「PRD 润色」「别做 spec 直接改代码」误触发 → 补负触发词面(润色/排版/直接改代码/别做 spec),trigger_eval 11/11 通过。
|
||||
2. **ledger.py 元组解包 bug**:append 直接崩溃 → 拆 resolve_ledger(),通过。
|
||||
3. **ledger 误报**:think 缺失警告未区分「Phase 3 合规跳过」→ 提示语区分,合规跳过需 state.md 有记录佐证。
|
||||
4. **Phase 4 独立性差异实测**:子代理执行环境(无嵌套 Agent 工具)自动降级自审并声明——机制正确,但自审 vs 独立审查的质量差是真实的(见测试 F),主会话使用时 Agent 工具可用,acceptance.md「首选子代理」规则已经覆盖。
|
||||
|
||||
## 已知限制与未验证项(如实声明)
|
||||
|
||||
1. **[假设→部分验证] 多 runtime**:ZCode 路径全程实测;Claude Code / Codex 仅建软链未实测(Skill 工具按名调用路径未跑)。
|
||||
2. **[假设] think 其它模式**:Evaluation / Triage / Lightweight 模式未被单独触发测试(全模式已两轮实测)。
|
||||
3. **[假设] openspec store**:--store 语义未测(本机无 store 环境)。
|
||||
4. **[已知局限] 嵌套子代理**:le-spec 跑在子代理内时 Phase 4/6 必然降级自审(子代理无 Agent 工具);主会话直接使用无此限制。
|
||||
5. **[已知边界] init 授权解释**:用户宽泛授权(「继续吧」)时,执行者可能将授权泛化到 openspec init 决策(场景 1 记录为「用户批准」,场景 2 无授权时正确显式三选一询问)。explicit 判定留给执行 agent,建议用户对 init 表态时明确。
|
||||
6. **[已知局限] 触发实测深度**:冒烟 11/11;真实对话自动触发未测(本 skill 为本会话期间创建,同会话无法模拟新会话触发——见 le-meta-skill 惯例:skill 触发一次性读入,需新会话验证)。
|
||||
7. **[已知边界] 访谈轮次上限语义**:未验证 12+ 轮超长访谈的检查点节奏与上下文膨胀行为。
|
||||
|
||||
## 结论
|
||||
|
||||
九项测试全部通过,其中 A-I 的核心机制均有文件级证据;「真实集成、不冒充」约束由账本+CLI 退出码双重背书。未验证项集中于多 runtime 与依赖 skill 的非主路径模式,已在 README 与本报告声明,不声称已测。
|
||||
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Le SPEC 依赖检查:skill 目录 + openspec CLI。
|
||||
|
||||
用法:
|
||||
python3 deps_check.py # 人读输出, 退出码 0=required 全就位, 1=有缺失
|
||||
python3 deps_check.py --json # 机器可读
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# 依赖清单: (name, level, source, install_cmd)
|
||||
SKILL_DEPS = [
|
||||
("grilling", "required", "mattpocock/skills", "npx skills@latest add mattpocock/skills --skill grilling grill-me grill-with-docs domain-modeling -g"),
|
||||
("domain-modeling", "recommended", "mattpocock/skills", "npx skills@latest add mattpocock/skills --skill domain-modeling -g"),
|
||||
("grill-me", "optional", "mattpocock/skills", None), # 入口薄壳
|
||||
("grill-with-docs", "optional", "mattpocock/skills", None), # 入口薄壳
|
||||
("think", "required", "tw93/Waza", "npx skills@latest add tw93/Waza --skill think -g"),
|
||||
("openspec-propose", "recommended", "Fission-AI/OpenSpec", "npx skills@latest add Fission-AI/OpenSpec --skill openspec-propose openspec-new-change openspec-update-change openspec-verify-change openspec-archive-change openspec-apply-change openspec-explore -g"),
|
||||
("openspec-new-change", "recommended", "Fission-AI/OpenSpec", None),
|
||||
("openspec-update-change", "recommended", "Fission-AI/OpenSpec", None),
|
||||
("openspec-verify-change", "recommended", "Fission-AI/OpenSpec", None),
|
||||
("openspec-archive-change", "recommended", "Fission-AI/OpenSpec", None),
|
||||
]
|
||||
|
||||
# 候选 skill 根目录(顺序即优先级); 可用 LESPEC_SKILL_PATHS 覆盖, 冒号分隔
|
||||
DEFAULT_ROOTS = [
|
||||
"~/.agents/skills",
|
||||
"~/.zcode/skills",
|
||||
"~/.claude/skills",
|
||||
"~/.codex/skills",
|
||||
]
|
||||
|
||||
|
||||
def skill_roots():
|
||||
env = os.environ.get("LESPEC_SKILL_PATHS")
|
||||
if env:
|
||||
return [Path(p).expanduser() for p in env.split(":") if p.strip()]
|
||||
return [Path(p).expanduser() for p in DEFAULT_ROOTS]
|
||||
|
||||
|
||||
def find_skill(name):
|
||||
"""返回 (resolved_skill_md_path | None, root_dir)。软链解析到真源。"""
|
||||
for root in skill_roots():
|
||||
cand = root / name / "SKILL.md"
|
||||
if cand.exists():
|
||||
try:
|
||||
return cand.resolve(), root
|
||||
except OSError:
|
||||
return cand, root
|
||||
return None, None
|
||||
|
||||
|
||||
def check_openspec_cli():
|
||||
exe = shutil.which("openspec")
|
||||
if not exe:
|
||||
return {"name": "openspec-cli", "level": "required", "ok": False,
|
||||
"detail": "openspec not in PATH",
|
||||
"install": "npm install -g @fission-ai/openspec@latest"}
|
||||
try:
|
||||
out = subprocess.run([exe, "--version"], capture_output=True, text=True,
|
||||
timeout=15)
|
||||
ver = (out.stdout or out.returncode and out.stderr or "").strip().splitlines()[-1] \
|
||||
if (out.stdout or out.stderr) else "?"
|
||||
return {"name": "openspec-cli", "level": "required", "ok": out.returncode == 0,
|
||||
"detail": f"{exe} (v{ver})",
|
||||
"install": "npm install -g @fission-ai/openspec@latest"}
|
||||
except Exception as e: # timeout / spawn 失败
|
||||
return {"name": "openspec-cli", "level": "required", "ok": False,
|
||||
"detail": f"exec failed: {e}",
|
||||
"install": "npm install -g @fission-ai/openspec@latest"}
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--json", action="store_true", help="JSON 输出")
|
||||
args = ap.parse_args()
|
||||
|
||||
results = []
|
||||
for name, level, source, install in SKILL_DEPS:
|
||||
path, root = find_skill(name)
|
||||
results.append({
|
||||
"name": name, "kind": "skill", "level": level,
|
||||
"ok": path is not None,
|
||||
"detail": str(path) if path else f"not found under {':'.join(str(r) for r in skill_roots())}",
|
||||
"source": source, "install": install,
|
||||
})
|
||||
results.append(check_openspec_cli())
|
||||
|
||||
missing_required = [r for r in results if not r["ok"] and r["level"] == "required"]
|
||||
missing_recommended = [r for r in results if not r["ok"] and r["level"] == "recommended"]
|
||||
|
||||
if args.json:
|
||||
print(json.dumps({
|
||||
"ok": not missing_required,
|
||||
"missing_required": [r["name"] for r in missing_required],
|
||||
"missing_recommended": [r["name"] for r in missing_recommended],
|
||||
"items": results,
|
||||
}, ensure_ascii=False, indent=2))
|
||||
else:
|
||||
mark = lambda r: "✅" if r["ok"] else ("❌" if r["level"] == "required" else "⚠️ ")
|
||||
print(f"{'':2}{'状态':4} {'级别':12} {'依赖':24} 详情")
|
||||
for r in results:
|
||||
print(f"{mark(r)} {r['level']:<12} {r['name']:<24} {r['detail']}")
|
||||
print()
|
||||
if not missing_required and not missing_recommended:
|
||||
print("全部依赖就位。")
|
||||
for r in missing_required + missing_recommended:
|
||||
if r.get("install"):
|
||||
print(f"补装 {r['name']}: {r['install']}")
|
||||
print("提示: optional 级(grill-me/grill-with-docs)是入口薄壳, 缺失不影响运行。")
|
||||
|
||||
sys.exit(1 if missing_required else 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,143 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Le SPEC 调用账本: 记录每次依赖真实交互, 供审查与交付作证据。
|
||||
|
||||
用法:
|
||||
python3 ledger.py append --dep grilling --kind skill-load --method file-read \
|
||||
--summary "Phase2 R1 frontier 4问" [--cmd "..."] [--exit 0] [--file PATH]
|
||||
python3 ledger.py check [--file PATH] [--json]
|
||||
python3 ledger.py tail [-n 5] [--file PATH]
|
||||
|
||||
账本定位优先级: --file > openspec root 下 openspec/le-spec/ledger.jsonl > ./.le-spec/ledger.jsonl
|
||||
"""
|
||||
import argparse
|
||||
import datetime
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
KINDS = {"skill-load", "cli", "subagent"}
|
||||
METHODS = {"skill-tool", "file-read", "bash", "agent"}
|
||||
|
||||
|
||||
def default_ledger():
|
||||
try:
|
||||
out = subprocess.run(["openspec", "context", "--json"],
|
||||
capture_output=True, text=True, timeout=15)
|
||||
ctx = json.loads(out.stdout or "{}")
|
||||
root = (ctx.get("root") or {}).get("path")
|
||||
if root:
|
||||
p = Path(root) / "openspec" / "le-spec" / "ledger.jsonl"
|
||||
return p, True
|
||||
except Exception:
|
||||
pass
|
||||
return Path.cwd() / ".le-spec" / "ledger.jsonl", False
|
||||
|
||||
|
||||
def resolve_ledger(args):
|
||||
"""返回 (path, has_openspec_root)。"""
|
||||
if getattr(args, "file", None):
|
||||
return Path(args.file).expanduser(), None
|
||||
return default_ledger()
|
||||
|
||||
|
||||
def cmd_append(args):
|
||||
if args.kind not in KINDS or args.method not in METHODS:
|
||||
print(f"kind 须为 {KINDS}, method 须为 {METHODS}", file=sys.stderr)
|
||||
return 2
|
||||
path, has_root = resolve_ledger(args)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
entry = {
|
||||
"ts": datetime.datetime.now().isoformat(timespec="seconds"),
|
||||
"phase": args.phase, "dep": args.dep, "kind": args.kind,
|
||||
"method": args.method, "cmd": args.cmd, "exit": args.exit,
|
||||
"summary": args.summary,
|
||||
}
|
||||
with path.open("a", encoding="utf-8") as f:
|
||||
f.write(json.dumps({k: v for k, v in entry.items() if v is not None},
|
||||
ensure_ascii=False) + "\n")
|
||||
print(f"账本已记: {path}")
|
||||
if has_root is False and not args.file:
|
||||
print("(未找到 openspec root, 暂写 .le-spec/; Phase 5 init 后迁移)", file=sys.stderr)
|
||||
return 0
|
||||
|
||||
|
||||
def _load(path):
|
||||
if not path.exists():
|
||||
return []
|
||||
rows = []
|
||||
for line in path.read_text(encoding="utf-8").splitlines():
|
||||
line = line.strip()
|
||||
if line:
|
||||
try:
|
||||
rows.append(json.loads(line))
|
||||
except json.JSONDecodeError:
|
||||
rows.append({"_unparsed": line})
|
||||
return rows
|
||||
|
||||
|
||||
def cmd_check(args):
|
||||
path, _ = resolve_ledger(args)
|
||||
rows = _load(path)
|
||||
by_dep = {}
|
||||
for r in rows:
|
||||
d = by_dep.setdefault(r.get("dep", "?"), {"count": 0, "last": None, "kinds": {}})
|
||||
d["count"] += 1
|
||||
d["last"] = r.get("ts", d["last"])
|
||||
d["kinds"][r.get("kind", "?")] = d["kinds"].get(r.get("kind", "?"), 0) + 1
|
||||
if args.json:
|
||||
print(json.dumps({"ledger": str(path), "total": len(rows), "by_dep": by_dep},
|
||||
ensure_ascii=False, indent=2))
|
||||
else:
|
||||
print(f"账本: {path} | 总计 {len(rows)} 条")
|
||||
for dep, d in sorted(by_dep.items()):
|
||||
kinds = ", ".join(f"{k}×{v}" for k, v in d["kinds"].items())
|
||||
print(f" {dep:<16} {d['count']:>3} 次 ({kinds}) 末次 {d['last']}")
|
||||
expected = ["grilling", "think", "openspec"]
|
||||
missing = [e for e in expected if e not in by_dep]
|
||||
if missing:
|
||||
hint = {"think": "(若 Phase 3 合规跳过——无实质方案分歧——属正常,确认 state.md 有跳过记录即可)"}
|
||||
for m in missing:
|
||||
print(f"⚠️ 未见 {m} 的调用记录 {hint.get(m, '')} — 否则 Phase 6 审查前需真实补调或如实声明")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_tail(args):
|
||||
path, _ = resolve_ledger(args)
|
||||
rows = _load(path)[-args.n:]
|
||||
for r in rows:
|
||||
print(json.dumps(r, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
sub = ap.add_subparsers(dest="cmd", required=True)
|
||||
|
||||
a = sub.add_parser("append")
|
||||
a.add_argument("--dep", required=True)
|
||||
a.add_argument("--kind", required=True)
|
||||
a.add_argument("--method", required=True)
|
||||
a.add_argument("--phase")
|
||||
a.add_argument("--cmd")
|
||||
a.add_argument("--exit", type=int)
|
||||
a.add_argument("--summary")
|
||||
a.add_argument("--file")
|
||||
a.set_defaults(fn=cmd_append)
|
||||
|
||||
c = sub.add_parser("check")
|
||||
c.add_argument("--file")
|
||||
c.add_argument("--json", action="store_true")
|
||||
c.set_defaults(fn=cmd_check)
|
||||
|
||||
t = sub.add_parser("tail")
|
||||
t.add_argument("-n", type=int, default=5)
|
||||
t.add_argument("--file")
|
||||
t.set_defaults(fn=cmd_tail)
|
||||
|
||||
args = ap.parse_args()
|
||||
sys.exit(args.fn(args))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user