diff --git a/.gitignore b/.gitignore index 37d1cfe..5851e71 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,6 @@ __pycache__/ # 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 diff --git a/skills/le-spec/README.md b/skills/le-spec/README.md new file mode 100644 index 0000000..c30e0eb --- /dev/null +++ b/skills/le-spec/README.md @@ -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 执行记录与未验证项)。 diff --git a/skills/le-spec/SKILL.md b/skills/le-spec/SKILL.md new file mode 100644 index 0000000..a76a92c --- /dev/null +++ b/skills/le-spec/SKILL.md @@ -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`。有根且存在 `/openspec/le-spec/state.md` → 读状态,从其「下一步」恢复,不重做已完成阶段。 +2. 无状态文件 → 全新分析:跑 `python3 /scripts/deps_check.py` 检查依赖(见铁律 1)。缺失 required 级依赖 → 停止并给出精确安装命令(见 dependencies.md),不得以降级模拟继续。 +3. 按 Phase 0 → 1 进入。 + +## 状态与账本(持久化,跨会话恢复) + +- `/openspec/le-spec/state.md` —— 阶段、已确认决策、否定方案、开放问题、假设、下一步、访谈轮次计数。每轮重要交互后增量更新;用户变更需求时记录影响范围。 +- `/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 或确认账本路径可写。 diff --git a/skills/le-spec/evals/trigger_cases.json b/skills/le-spec/evals/trigger_cases.json new file mode 100644 index 0000000..bfca890 --- /dev/null +++ b/skills/le-spec/evals/trigger_cases.json @@ -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"} + ] +} diff --git a/skills/le-spec/references/acceptance.md b/skills/le-spec/references/acceptance.md new file mode 100644 index 0000000..1f52e74 --- /dev/null +++ b/skills/le-spec/references/acceptance.md @@ -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。 diff --git a/skills/le-spec/references/dependencies.md b/skills/le-spec/references/dependencies.md new file mode 100644 index 0000000..296de2b --- /dev/null +++ b/skills/le-spec/references/dependencies.md @@ -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 --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 "" # 脚手架(默认 spec-driven schema) +openspec status --change "" --json # artifact 顺序与路径 +openspec instructions --change "" --json # 逐个:proposal → specs → design → tasks +# 按 template + instruction 撰写每个 artifact 文件 +openspec validate "" # 必须通过 +``` + +- **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 的阶段标记,已完成阶段不重做(中断恢复同理)。 diff --git a/skills/le-spec/references/review.md b/skills/le-spec/references/review.md new file mode 100644 index 0000000..bde513d --- /dev/null +++ b/skills/le-spec/references/review.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 ""`(严格模式可用则用)+ `openspec status --change ""` 确认 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 流程处理。 diff --git a/skills/le-spec/references/workflow.md b/skills/le-spec/references/workflow.md new file mode 100644 index 0000000..cfb5c98 --- /dev/null +++ b/skills/le-spec/references/workflow.md @@ -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` → 读 `/openspec/le-spec/state.md` → 校验其「下一步」与 OpenSpec 实际状态(`openspec status --change` 或 `openspec list --json`)一致 → 从断点继续。state.md 阶段标记与实际不符时以实际产物为准并修正状态。 diff --git a/skills/le-spec/reports/README.md b/skills/le-spec/reports/README.md new file mode 100644 index 0000000..942a2cd --- /dev/null +++ b/skills/le-spec/reports/README.md @@ -0,0 +1,6 @@ +# reports/ —— 证据政策 + +- `test-report.md` —— 测试 A-I 的执行记录、修正记录与已知限制(**入库**:本次交付要求的可复查证据) +- `trigger-eval.json` —— trigger_eval.py 生成物(**不入库**,gitignore:按需重跑即得,遵循仓库先例) + +诚实条款:验证不了的写「证据缺失」,不编造;计划中的工作不是证据。测试脚手架项目(/tmp/lespec-test/)不入库,证据以本报告 + 文中账本数字为指针。 diff --git a/skills/le-spec/reports/test-report.md b/skills/le-spec/reports/test-report.md new file mode 100644 index 0000000..124bef2 --- /dev/null +++ b/skills/le-spec/reports/test-report.md @@ -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 与本报告声明,不声称已测。 diff --git a/skills/le-spec/scripts/deps_check.py b/skills/le-spec/scripts/deps_check.py new file mode 100644 index 0000000..35ea7f9 --- /dev/null +++ b/skills/le-spec/scripts/deps_check.py @@ -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() diff --git a/skills/le-spec/scripts/ledger.py b/skills/le-spec/scripts/ledger.py new file mode 100644 index 0000000..8b4d0c6 --- /dev/null +++ b/skills/le-spec/scripts/ledger.py @@ -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()