feat: rename bugfix-ship/requirement-flow to short-ship/long-ship with shared ship records
short-ship and long-ship share one ship-id record under .pouch/ship/<id>/. specifier writes PRDs into spec/; delivery-loop appends evidence pointers. Old names stay as deprecation stubs so existing installs keep working.
This commit is contained in:
@@ -61,12 +61,19 @@ AGENTS.md # 本文档
|
|||||||
| [orc](skills/orc/README.md) | 把开发、发版、DEB / Docker 意图编排成阶段,按档位分发给对应 skill |
|
| [orc](skills/orc/README.md) | 把开发、发版、DEB / Docker 意图编排成阶段,按档位分发给对应 skill |
|
||||||
| [pouch](skills/pouch/README.md) | 创建、校验、安装和维护 builtin skill |
|
| [pouch](skills/pouch/README.md) | 创建、校验、安装和维护 builtin skill |
|
||||||
| [previewer](skills/previewer/README.md) | 通过 Tailscale Serve 将静态文件暴露为 tailnet 内 HTTP 预览 |
|
| [previewer](skills/previewer/README.md) | 通过 Tailscale Serve 将静态文件暴露为 tailnet 内 HTTP 预览 |
|
||||||
|
| [pr-ship](skills/pr-ship/README.md) | 把已有 PR 从检查、合并推进到回归、构建和部署 |
|
||||||
| [releaser](skills/releaser/README.md) | 给已准备好的 commit 发版本:更新说明、tag、可选 Forge Release |
|
| [releaser](skills/releaser/README.md) | 给已准备好的 commit 发版本:更新说明、tag、可选 Forge Release |
|
||||||
| [specifier](skills/specifier/README.md) | 把复杂需求整理成多轮评审的 HTML PRD,并在批准后生成 ACK handoff |
|
| [ship](skills/ship/README.md) | short-ship / long-ship / pr-ship 共用的交付记录格式 |
|
||||||
|
| [short-ship](skills/short-ship/README.md) | 范围清楚的单次变更:小功能、文案、已确认 bug,一直交到停点 |
|
||||||
|
| [long-ship](skills/long-ship/README.md) | 复杂需求:specifier PRD 批准后拆任务、集成回归并交付 |
|
||||||
|
| [specifier](skills/specifier/README.md) | 把复杂需求整理成多轮评审的 HTML PRD,批准后交给 long-ship |
|
||||||
|
| [delivery-loop](skills/delivery-loop/README.md) | 三个交付入口共用的验证到部署状态机 |
|
||||||
| [manage-release](skills/manage-release/README.md) | 已弃用:`releaser` 的旧名兼容占位,不含流程 |
|
| [manage-release](skills/manage-release/README.md) | 已弃用:`releaser` 的旧名兼容占位,不含流程 |
|
||||||
|
| [bugfix-ship](skills/bugfix-ship/README.md) | 已弃用:`short-ship` 的旧名兼容占位,不含流程 |
|
||||||
|
| [requirement-flow](skills/requirement-flow/README.md) | 已弃用:`long-ship` 的旧名兼容占位,不含流程 |
|
||||||
|
|
||||||
改名后旧名保留一个纯占位 skill,避免已装软链悬空和旧项目 `.pouch.yaml` 失效。确认没有
|
改名后旧名保留一个纯占位 skill,避免已装软链悬空和旧项目 `.pouch.yaml` 失效。确认没有
|
||||||
项目再按 `manage-release` 启用后,删除 `skills/manage-release/` 即可,不丢任何内容。
|
项目再按旧名启用后,删除对应占位目录即可,不丢任何内容。
|
||||||
|
|
||||||
|
|
||||||
新建 skill:优先 `pouch create`;或复制 `skills/_template/` → `skills/<name>/`,编辑 `SKILL.md` 和 `README.md`,在本仓库 commit。
|
新建 skill:优先 `pouch create`;或复制 `skills/_template/` → `skills/<name>/`,编辑 `SKILL.md` 和 `README.md`,在本仓库 commit。
|
||||||
|
|||||||
@@ -97,9 +97,16 @@ AGENTS.md # 详细规范与架构说明
|
|||||||
| [orc](skills/orc/README.md) | 把开发、发版、DEB / Docker 意图编排成阶段,按档位分发给对应 skill |
|
| [orc](skills/orc/README.md) | 把开发、发版、DEB / Docker 意图编排成阶段,按档位分发给对应 skill |
|
||||||
| [pouch](skills/pouch/README.md) | 创建、校验、安装和维护 builtin skill |
|
| [pouch](skills/pouch/README.md) | 创建、校验、安装和维护 builtin skill |
|
||||||
| [previewer](skills/previewer/README.md) | 用 Node 渲染 HTML/静态文件,并通过 Tailscale 返回访问 IP 和 HTTP 预览 |
|
| [previewer](skills/previewer/README.md) | 用 Node 渲染 HTML/静态文件,并通过 Tailscale 返回访问 IP 和 HTTP 预览 |
|
||||||
|
| [pr-ship](skills/pr-ship/README.md) | 把已有 PR 从检查、合并推进到回归、构建和部署 |
|
||||||
| [releaser](skills/releaser/README.md) | 给已准备好的 commit 发版本:更新说明、tag、可选 Forge Release |
|
| [releaser](skills/releaser/README.md) | 给已准备好的 commit 发版本:更新说明、tag、可选 Forge Release |
|
||||||
| [specifier](skills/specifier/README.md) | 把复杂需求整理成多轮评审的 HTML PRD,并在批准后生成 ACK handoff |
|
| [ship](skills/ship/README.md) | short-ship / long-ship / pr-ship 共用的交付记录格式 |
|
||||||
|
| [short-ship](skills/short-ship/README.md) | 范围清楚的单次变更:小功能、文案、已确认 bug |
|
||||||
|
| [long-ship](skills/long-ship/README.md) | 复杂需求:PRD 批准后拆任务、集成回归并交付 |
|
||||||
|
| [specifier](skills/specifier/README.md) | 把复杂需求整理成多轮评审的 HTML PRD,批准后交给 long-ship |
|
||||||
|
| [delivery-loop](skills/delivery-loop/README.md) | 三个交付入口共用的验证到部署状态机 |
|
||||||
| [manage-release](skills/manage-release/README.md) | 已弃用:`releaser` 的旧名兼容占位,不含流程 |
|
| [manage-release](skills/manage-release/README.md) | 已弃用:`releaser` 的旧名兼容占位,不含流程 |
|
||||||
|
| [bugfix-ship](skills/bugfix-ship/README.md) | 已弃用:`short-ship` 的旧名兼容占位,不含流程 |
|
||||||
|
| [requirement-flow](skills/requirement-flow/README.md) | 已弃用:`long-ship` 的旧名兼容占位,不含流程 |
|
||||||
|
|
||||||
|
|
||||||
ACK 是包含规范、模板与校验脚本的完整 Skill。安装后可初始化当前项目状态:
|
ACK 是包含规范、模板与校验脚本的完整 Skill。安装后可初始化当前项目状态:
|
||||||
|
|||||||
+9
-1
@@ -59,6 +59,10 @@ def _parse_scalar(raw: str) -> Any:
|
|||||||
return True
|
return True
|
||||||
if lower == "false":
|
if lower == "false":
|
||||||
return False
|
return False
|
||||||
|
if raw == "[]":
|
||||||
|
return []
|
||||||
|
if raw == "{}":
|
||||||
|
return {}
|
||||||
if raw.isdigit() or (raw.startswith("-") and raw[1:].isdigit()):
|
if raw.isdigit() or (raw.startswith("-") and raw[1:].isdigit()):
|
||||||
return int(raw)
|
return int(raw)
|
||||||
return raw
|
return raw
|
||||||
@@ -196,7 +200,11 @@ def _dump(data: Any, indent: int = 0) -> str:
|
|||||||
return f"{pad}{{}}\n"
|
return f"{pad}{{}}\n"
|
||||||
lines: list[str] = []
|
lines: list[str] = []
|
||||||
for key, value in data.items():
|
for key, value in data.items():
|
||||||
if isinstance(value, (dict, list)):
|
if isinstance(value, list) and not value:
|
||||||
|
lines.append(f"{pad}{key}: []")
|
||||||
|
elif isinstance(value, dict) and not value:
|
||||||
|
lines.append(f"{pad}{key}: {{}}")
|
||||||
|
elif isinstance(value, (dict, list)):
|
||||||
if isinstance(value, list) and value and all(isinstance(x, str) for x in value):
|
if isinstance(value, list) and value and all(isinstance(x, str) for x in value):
|
||||||
lines.append(f"{pad}{key}:")
|
lines.append(f"{pad}{key}:")
|
||||||
for item in value:
|
for item in value:
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ ACK 是显式调用的 Agent Skill,用三种独立角色跑工程协作闭环
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| 初始化 | `/ack 初始化` | 生成并补全 `.pouch/ack/`;缺项按统一格式列出 |
|
| 初始化 | `/ack 初始化` | 生成并补全 `.pouch/ack/`;缺项按统一格式列出 |
|
||||||
| 检查 | `/ack 检查配置` | 只读校验,默认不改文件;测试环境缺 deployer 时转交 |
|
| 检查 | `/ack 检查配置` | 只读校验,默认不改文件;测试环境缺 deployer 时转交 |
|
||||||
| 做需求 | `/ack 处理这个需求:…` | 产品文档 + 拆任务 → 你确认 → 三角色闭环 |
|
| 做需求 | `/ack 处理这个需求:…` | 先建 ship 记录;复杂需求走 specifier / long-ship,小改动走 short-ship |
|
||||||
| 修 bug | `/ack 修这个 bug:…` 或处理飞书收件 | 短描述 + 验收 → 你确认(飞书须你点「已确认」)→ 同一闭环 |
|
| 修 bug | `/ack 修这个 bug:…` 或处理飞书收件 | 短描述 + 验收 → 你确认(飞书须你点「已确认」)→ 缺陷板闭环,交付链接 short-ship |
|
||||||
| 交付配置 | 说明怎么布测试环境 / 怎么发版 | 写入同一份 `delivery.yaml`;测试环境绑定 deployer |
|
| 交付配置 | 说明怎么布测试环境 / 怎么发版 | 写入同一份 `delivery.yaml`;测试环境绑定 deployer |
|
||||||
| 运行测试环境 | `/ack 重新布测试环境` | 内部加载 deployer,布 `.pouch/deployer/<env>` |
|
| 运行测试环境 | `/ack 重新布测试环境` | 内部加载 deployer,布 `.pouch/deployer/<env>` |
|
||||||
| 运行版本发布 | `/ack 发布一个版本` | 按 `intents.release`;stable/生产仍要单独批准 |
|
| 运行版本发布 | `/ack 发布一个版本` | 按 `intents.release`;stable/生产仍要单独批准 |
|
||||||
@@ -117,7 +117,7 @@ ACK 会在 `.pouch/ack/agents.yaml` 保存结构化覆盖配置。它存在时
|
|||||||
## Agent 会做什么
|
## Agent 会做什么
|
||||||
|
|
||||||
1. 初始化或检查 `.pouch/ack/`,缺项按统一格式列出;不从 README/CI 猜测并启用交付或知识。
|
1. 初始化或检查 `.pouch/ack/`,缺项按统一格式列出;不从 README/CI 猜测并启用交付或知识。
|
||||||
2. 做需求:先写产品文档、任务拆分和可观测验收;修 bug 只写短说明、复现和验收。你确认后再派 Developer 和独立 Test。
|
2. 做需求:规格和任务写到 `.pouch/ship/<id>/`;修 bug 只写短说明、复现和验收,落缺陷板。你确认后再派 Developer 和独立 Test。
|
||||||
3. 需要测试环境时加载 deployer,不自己编 compose,也不把本地 `npm run dev` 当成测试环境。
|
3. 需要测试环境时加载 deployer,不自己编 compose,也不把本地 `npm run dev` 当成测试环境。
|
||||||
4. 任务 `verified` 后提议把本轮黑盒路径写入回归目录,你确认后才写。
|
4. 任务 `verified` 后提议把本轮黑盒路径写入回归目录,你确认后才写。
|
||||||
5. 发版读同一份 `delivery.yaml`。默认最多停在 `validation_ready` 或 `review_ready`;稳定发布或生产部署必须再经过对应批准。
|
5. 发版读同一份 `delivery.yaml`。默认最多停在 `validation_ready` 或 `review_ready`;稳定发布或生产部署必须再经过对应批准。
|
||||||
|
|||||||
+8
-2
@@ -34,7 +34,8 @@ description: >-
|
|||||||
- 初始化、接入 ACK:执行「初始化」。
|
- 初始化、接入 ACK:执行「初始化」。
|
||||||
- 检查配置是否完整:执行「检查」。
|
- 检查配置是否完整:执行「检查」。
|
||||||
- 临时调整 Developer/Test 的 CLI 或模型:执行「角色启动配置维护」。
|
- 临时调整 Developer/Test 的 CLI 或模型:执行「角色启动配置维护」。
|
||||||
- 做需求、修 bug 或继续任务:执行「工作」。修 bug 不写大 PRD。
|
- 做需求、修 bug 或继续任务:执行「工作」。修 bug 不写大 PRD。新功能和小改动的执行记录写
|
||||||
|
`.pouch/ship/`,不要写进缺陷任务板。
|
||||||
- 增改关闭交付、说明怎么布测试环境或发版:执行「交付配置维护」。
|
- 增改关闭交付、说明怎么布测试环境或发版:执行「交付配置维护」。
|
||||||
- 部署或重布测试环境:执行「运行测试环境」(加载 deployer skill 的「初始化」或运行流程,不复制 compose)。
|
- 部署或重布测试环境:执行「运行测试环境」(加载 deployer skill 的「初始化」或运行流程,不复制 compose)。
|
||||||
- 发布版本:执行「运行版本发布」。回归:执行「运行回归」。
|
- 发布版本:执行「运行版本发布」。回归:执行「运行回归」。
|
||||||
@@ -55,6 +56,9 @@ description: >-
|
|||||||
- 自动 worker 禁止 full-access、bypass、关闭 sandbox、Grok `--yolo` / bypassPermissions。OMP `--approval-mode yolo` 只用于 OMP 审批(workspace-write → yolo,read-only → always-ask),禁止 `--auto-approve`,不适用于其它 backend。
|
- 自动 worker 禁止 full-access、bypass、关闭 sandbox、Grok `--yolo` / bypassPermissions。OMP `--approval-mode yolo` 只用于 OMP 审批(workspace-write → yolo,read-only → always-ask),禁止 `--auto-approve`,不适用于其它 backend。
|
||||||
- 无清理证明不复用 worker;无密钥 `receiptHash` 或 Orca live metadata 不能授权复用。当前 Orca 走 fresh。只复用同轮、空闲且完成清理历史消息的 worker;无法确认清理成功时创建 fresh worker。`indeterminate` / `reconcile required` 不直接重试。
|
- 无清理证明不复用 worker;无密钥 `receiptHash` 或 Orca live metadata 不能授权复用。当前 Orca 走 fresh。只复用同轮、空闲且完成清理历史消息的 worker;无法确认清理成功时创建 fresh worker。`indeterminate` / `reconcile required` 不直接重试。
|
||||||
- 产品失败才占三轮;环境失败写 `environmentIncidents`,环境失败不占复验轮次。见 [optimization-method.md](references/optimization-method.md) §4。
|
- 产品失败才占三轮;环境失败写 `environmentIncidents`,环境失败不占复验轮次。见 [optimization-method.md](references/optimization-method.md) §4。
|
||||||
|
- `.pouch/ack/tasks.yaml` 收缺陷(飞书、回归失败、leftover)。功能、小改动和本次交付写
|
||||||
|
`.pouch/ship/<ship-id>/`。显式 `/ack` 做需求时先建 ship 记录;需要三角色时用 ACK 编排,
|
||||||
|
任务状态仍写在该 ship 的 `tasks.yaml`。缺陷要交付时在 ship `source` 引用 ACK 任务 id。
|
||||||
|
|
||||||
## 初始化
|
## 初始化
|
||||||
|
|
||||||
@@ -104,7 +108,9 @@ python3 <ack-skill-dir>/scripts/record_task.py .pouch/ack/tasks.yaml \
|
|||||||
|
|
||||||
1. `.pouch/ack` 不存在:停止并建议先 `/ack` 初始化;不要静默初始化。
|
1. `.pouch/ack` 不存在:停止并建议先 `/ack` 初始化;不要静默初始化。
|
||||||
2. 读 `project.md`;若存在 `agents.yaml`,先用 `validate_agents.py` 校验并读取其 `defaults`;用 `select_tasks.py`(已知任务加 `--task-id`)和 `select_knowledge.py` 取当前任务相关 `active` 条目。超预算时缩小选择,不回退为完整 yaml。
|
2. 读 `project.md`;若存在 `agents.yaml`,先用 `validate_agents.py` 校验并读取其 `defaults`;用 `select_tasks.py`(已知任务加 `--task-id`)和 `select_knowledge.py` 取当前任务相关 `active` 条目。超预算时缩小选择,不回退为完整 yaml。
|
||||||
3. 新需求或尚未确认的计划:读 [kickoff.md](references/kickoff.md)。修 bug 写短问题说明、复现和可观测验收,不写大 PRD。
|
3. 新需求或尚未确认的计划:读 [kickoff.md](references/kickoff.md)。范围清楚的小改动转
|
||||||
|
`short-ship`;复杂需求转 `specifier` / `long-ship`。修 bug 写短问题说明、复现和可观测
|
||||||
|
验收,不写大 PRD;要交付时链接 `short-ship`。
|
||||||
4. 用户已确认后,先读取并按 [closed-loop.md](references/closed-loop.md) 执行;使用 Orca 时同时读取 [orca-adapter.md](references/orca-adapter.md)。当前会话担任 Coordinator,不亲自写代码或跑测试;worker profile 按 `agents.yaml`(若存在)否则按任务板配置选择。
|
4. 用户已确认后,先读取并按 [closed-loop.md](references/closed-loop.md) 执行;使用 Orca 时同时读取 [orca-adapter.md](references/orca-adapter.md)。当前会话担任 Coordinator,不亲自写代码或跑测试;worker profile 按 `agents.yaml`(若存在)否则按任务板配置选择。
|
||||||
这不是 fire-and-forget:Coordinator 不得在 `launch` / `dispatch` 后只回复“已派发”并结束当前会话。每个任务都必须在同一轮持续执行默认 60 秒窗口的 `check --wait`,并按 60–120 秒节奏运行 `worker_probe.py`,直到 Developer 回报 `worker_done` 或 `escalation`;随后写回 `fixed_by_dev`,主动派发 Test,并继续等待 `retest_result`。只有完成 Test 终检、重试/`leftover` 决策、回归决定和 worker 回收后,才可以结束本轮 ACK。
|
这不是 fire-and-forget:Coordinator 不得在 `launch` / `dispatch` 后只回复“已派发”并结束当前会话。每个任务都必须在同一轮持续执行默认 60 秒窗口的 `check --wait`,并按 60–120 秒节奏运行 `worker_probe.py`,直到 Developer 回报 `worker_done` 或 `escalation`;随后写回 `fixed_by_dev`,主动派发 Test,并继续等待 `retest_result`。只有完成 Test 终检、重试/`leftover` 决策、回归决定和 worker 回收后,才可以结束本轮 ACK。
|
||||||
5. 存在 `project.bugIntake` 时先完成「飞书收件」。创建或更换 worker 走「启动 worker」。若 `intents.testEnvironment` 已启用,派 Test 前先走「运行测试环境」。
|
5. 存在 `project.bugIntake` 时先完成「飞书收件」。创建或更换 worker 走「启动 worker」。若 `intents.testEnvironment` 已启用,派 Test 前先走「运行测试环境」。
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
用户确认后的三角色闭环、飞书收件、启动 worker、交付和回归由 `SKILL.md` 按条件加载,
|
用户确认后的三角色闭环、飞书收件、启动 worker、交付和回归由 `SKILL.md` 按条件加载,
|
||||||
不要在本文件开头预读其它规范。
|
不要在本文件开头预读其它规范。
|
||||||
|
|
||||||
|
复杂需求的规格和任务写在 `.pouch/ship/<ship-id>/`(specifier + long-ship)。ACK 任务板
|
||||||
|
只收缺陷。显式 `/ack` 做功能需求时也先建 ship 记录,不要把功能任务写成 `type: bug`。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 前提:谁是 Coordinator
|
## 前提:谁是 Coordinator
|
||||||
|
|||||||
@@ -1,29 +1,24 @@
|
|||||||
# bugfix-ship
|
# bugfix-ship(已弃用)
|
||||||
|
|
||||||
把一个已确认的 bug 从根因分析一路推进到配置允许的 PR、合并或生产部署。
|
`bugfix-ship` 已改名为 [short-ship](../short-ship/README.md)。本目录只是兼容占位,
|
||||||
|
让旧项目里的 `.pouch.yaml`、已经装好的旧软链和旧说法不至于失效;真正的说明和流程都在
|
||||||
|
`short-ship/`。
|
||||||
|
|
||||||
## 什么时候使用
|
## 使用示例
|
||||||
|
|
||||||
```text
|
```text
|
||||||
修复动态编辑页重新打开后图片丢失的问题,测完后发 PR。
|
使用 bugfix-ship,修复这个线上回归并部署到测试环境。
|
||||||
```
|
```
|
||||||
|
|
||||||
或:
|
Agent 命中后会立刻转到 `short-ship`,按同一套规则执行。新请求建议直接用新名。
|
||||||
|
|
||||||
```text
|
## 迁移
|
||||||
修复这个线上回归,完整验证并部署到生产。
|
|
||||||
|
```bash
|
||||||
|
pouch add short-ship # 新名,当前项目
|
||||||
|
pouch add short-ship -g # 新名,全局
|
||||||
```
|
```
|
||||||
|
|
||||||
## 使用前准备
|
- 旧软链保持有效,不需要先删除。
|
||||||
|
- 各项目 `.pouch.yaml` 里的 `bugfix-ship` 可以分批改成 `short-ship`。
|
||||||
- 能复现的问题描述、截图或日志。
|
- 确认没有项目再按旧名启用后,删除本目录即可;它是纯占位,删除不丢内容。
|
||||||
- 项目已配置测试环境和所需 builder/tester/deployer 能力。
|
|
||||||
- 若要外部发布,明确允许的停止点或使用项目 profile。
|
|
||||||
|
|
||||||
## Agent 会做什么
|
|
||||||
|
|
||||||
查根因、改代码、补回归测试、执行必要的黑盒/真机验证,并继续完成配置允许的交付阶段。
|
|
||||||
|
|
||||||
## 如何判断完成
|
|
||||||
|
|
||||||
你会得到根因、修复 commit、测试证据、PR/合并/tag、产物校验和部署状态;不会只收到“本地测试通过”。
|
|
||||||
|
|||||||
+12
-43
@@ -1,50 +1,19 @@
|
|||||||
---
|
---
|
||||||
name: bugfix-ship
|
name: bugfix-ship
|
||||||
description: >-
|
description: >-
|
||||||
处理已复现或有证据的 bug:查根因、修复、补回归测试、验证并按配置继续发 PR、合并、
|
【已弃用,兼容占位】bugfix-ship 已改名为 short-ship。命中本 skill 时不要在此执行任何
|
||||||
构建、发布和部署。用户说修复、修好、回归、上线 bug 时使用;不处理新需求规划。
|
流程,直接读取 ../short-ship/SKILL.md 并按其执行。小需求、小改动、修 bug、改文案、
|
||||||
|
加按钮、修复后发 PR 或部署都属于 short-ship。
|
||||||
---
|
---
|
||||||
|
|
||||||
# Bugfix Ship:修复到交付
|
# bugfix-ship(已弃用)
|
||||||
|
|
||||||
将一次 bug 修复推进到项目配置允许的最终停止点,而不是在本地测试通过后提前结束。
|
本 skill 已改名为 [short-ship](../short-ship/SKILL.md)。这里只是兼容占位,**没有流程内容**。
|
||||||
|
|
||||||
## 何时使用
|
- 用户提到 `bugfix-ship`、`/bugfix-ship` 或 `$bugfix-ship` 时:读取
|
||||||
|
[../short-ship/SKILL.md](../short-ship/SKILL.md),按其内容与授权边界执行,不要在本目录
|
||||||
- 用户报告错误、回归、崩溃、截图缺陷或明确要求修 bug。
|
里寻找步骤、references 或 templates。
|
||||||
- 用户要求修复后继续测试、发 PR、合并、构建、发布或部署。
|
- 本目录不保留 `references/` 和 `templates/`,正文只有一份,在 `short-ship/`。
|
||||||
|
- 旧项目 `.pouch.yaml` 里仍写 `bugfix-ship` 也能正常安装,软链不会悬空;方便时改成
|
||||||
## 不适用
|
`short-ship`。
|
||||||
|
- 确认没有项目再按旧名启用后,可以直接删除本目录。
|
||||||
- 尚未定义目标的复杂新需求:交给 `specifier` 和 `requirement-flow`。
|
|
||||||
- 已有 PR 的合并和发布:交给 `pr-ship`。
|
|
||||||
- 只做根因分析、不改代码:使用 `hunt`。
|
|
||||||
|
|
||||||
## 工作规则
|
|
||||||
|
|
||||||
1. 读取项目约束、当前分支/工作树和相关任务;使用 `hunt` 确认根因、复现和验收信号。
|
|
||||||
2. 修改代码并补充或更新回归用例;行为、API、图片、UI 改动不得只跑编译。
|
|
||||||
3. 使用 `tester` 执行黑盒、浏览器或真机验证;涉及资源加载时检查真实 URL、状态、类型、
|
|
||||||
内容和截图,不把接口返回“有 URL”当作图片可用。
|
|
||||||
4. 读取并执行共享 `delivery-loop` 的状态机。停止点由用户选择或项目 delivery profile
|
|
||||||
决定,使用 ACK canonical 状态:`verified`、`validation_ready`、`review_ready` 或 `released`。
|
|
||||||
5. 只有达到配置的停止点并拿到证据后才结束;提交、推送、合并、发布和部署不能静默跳过。
|
|
||||||
需要产物时只调用 builder `obtain.sh --sha <merged-or-pushed-commit>`:CI 项目等该
|
|
||||||
SHA 的流水线,失败(无流水线、job 失败、digest 不对)则停止,不要本机 `docker push`。
|
|
||||||
合进默认分支若项目不构建,把预览推到 `dev/*` 或等 releaser 打 tag。
|
|
||||||
|
|
||||||
## 安全门
|
|
||||||
|
|
||||||
- 根因不清、测试缺失、真机不可达、CI/回归失败或工作树有无关改动时停止并说明。
|
|
||||||
- 不强推、不覆盖他人分支、不绕过 PR 审核,不把测试环境成功当生产成功。
|
|
||||||
- 数据迁移、破坏性 API、权限/密钥、安全修复和不可逆生产操作需要额外确认。
|
|
||||||
|
|
||||||
## 必读资料
|
|
||||||
|
|
||||||
- 执行交付阶段时读取已安装的 `delivery-loop` skill 及其状态机 reference。
|
|
||||||
- 测试环境或回归时使用项目的 `tester`、`deployer` 和 `builder` 契约。
|
|
||||||
- 提交、合并或发布前使用 `check` 与 `releaser` 的规则。
|
|
||||||
|
|
||||||
## 完成报告
|
|
||||||
|
|
||||||
报告根因、变更、测试/真机证据、停止点、commit、PR/tag、产物摘要、部署状态和任何未完成项。
|
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
interface:
|
|
||||||
display_name: "Bugfix Ship"
|
|
||||||
short_description: "Carry a bug fix through verification and delivery"
|
|
||||||
default_prompt: "Use $bugfix-ship to fix this bug, verify it, and continue to the configured delivery stop."
|
|
||||||
policy:
|
|
||||||
allow_implicit_invocation: true
|
|
||||||
@@ -35,7 +35,7 @@ worktree,也可以指向已配对的远程 Mac/其它构建机;不要把远
|
|||||||
- 初始化、接入 builder,或还没有 `makefile.builder`:执行「初始化」。
|
- 初始化、接入 builder,或还没有 `makefile.builder`:执行「初始化」。
|
||||||
- 检查契约或发布配置:执行「检查」。
|
- 检查契约或发布配置:执行「检查」。
|
||||||
- 构建、发布、上传:执行「工作流」。不要静默初始化。
|
- 构建、发布、上传:执行「工作流」。不要静默初始化。
|
||||||
- 看构建、监听 CI、ACK/bugfix-ship 要产物:执行「拿到产物」。不要当成第五条轨道。
|
- 看构建、监听 CI、ACK/short-ship/long-ship 要产物:执行「拿到产物」。不要当成第五条轨道。
|
||||||
- 指定 `builder.<name>`、远程构建机或跨 session:先读共享 `agent-runtime`。
|
- 指定 `builder.<name>`、远程构建机或跨 session:先读共享 `agent-runtime`。
|
||||||
- 不适用:本地安装/卸载 DEB;RPM/语言包;从零设计打包体系(先出方案);普通编码与 Dockerfile 编辑。
|
- 不适用:本地安装/卸载 DEB;RPM/语言包;从零设计打包体系(先出方案);普通编码与 Dockerfile 编辑。
|
||||||
- 改本 skill 自身:契约先改 `scripts/check.py`,再同步 [contract.md](references/contract.md) 与 templates;不在生产上传上试脚本。
|
- 改本 skill 自身:契约先改 `scripts/check.py`,再同步 [contract.md](references/contract.md) 与 templates;不在生产上传上试脚本。
|
||||||
@@ -86,7 +86,7 @@ COPY_AS=latest.apk
|
|||||||
|
|
||||||
## 拿到产物
|
## 拿到产物
|
||||||
|
|
||||||
调用方是 bugfix-ship、ACK(delivery-loop 的 `artifact_built`),或用户要看 CI 构建。
|
调用方是 short-ship、long-ship、ACK(delivery-loop 的 `artifact_built`),或用户要看 CI 构建。
|
||||||
按 [obtain.md](references/obtain.md) 执行 `scripts/obtain.sh --sha <commit>`。
|
按 [obtain.md](references/obtain.md) 执行 `scripts/obtain.sh --sha <commit>`。
|
||||||
|
|
||||||
- `BUILD_VENUE=local`:当前 HEAD 必须是该 SHA,跑 makefile 的 docker/deb。
|
- `BUILD_VENUE=local`:当前 HEAD 必须是该 SHA,跑 makefile 的 docker/deb。
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ docker / deb / android / ios / copy 仍是轨道;`BUILD_VENUE` 只说明构建
|
|||||||
|
|
||||||
## 谁来调用
|
## 谁来调用
|
||||||
|
|
||||||
- `bugfix-ship` 和 ACK 走到 delivery-loop 的 `artifact_built` 时。
|
- `short-ship` / `long-ship` 和 ACK 走到 delivery-loop 的 `artifact_built` 时。
|
||||||
- 用户说「看构建」「监听 CI」「拿到镜像」时。
|
- 用户说「看构建」「监听 CI」「拿到镜像」时。
|
||||||
- `make -f makefile.builder obtain` 或 `push-docker`(`BUILD_VENUE=ci`)。
|
- `make -f makefile.builder obtain` 或 `push-docker`(`BUILD_VENUE=ci`)。
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,15 @@
|
|||||||
|
|
||||||
## 什么时候使用
|
## 什么时候使用
|
||||||
|
|
||||||
通常由 `$bugfix-ship`、`$pr-ship` 或 `$requirement-flow` 内部读取,不需要单独调用。
|
通常由 `$short-ship`、`$pr-ship` 或 `$long-ship` 内部读取,不需要单独调用。
|
||||||
|
|
||||||
|
```text
|
||||||
|
按 delivery-loop 把当前 ship 推进到 review_ready。
|
||||||
|
```
|
||||||
|
|
||||||
## 使用前准备
|
## 使用前准备
|
||||||
|
|
||||||
|
- 入口已绑定 ship id 时,同时安装 `ship`,证据指针写进该记录。
|
||||||
- 项目已有有效的 ACK delivery profile 或等价交付配置。
|
- 项目已有有效的 ACK delivery profile 或等价交付配置。
|
||||||
- 已明确任务的目标分支、测试范围和允许的停止点。
|
- 已明确任务的目标分支、测试范围和允许的停止点。
|
||||||
- 需要外部写入时,当前请求已经授权对应动作。
|
- 需要外部写入时,当前请求已经授权对应动作。
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
name: delivery-loop
|
name: delivery-loop
|
||||||
description: >-
|
description: >-
|
||||||
为修复、PR 和需求任务提供可配置的验证、发 PR、合并、回归、构建、发布与部署状态机。
|
为修复、PR 和需求任务提供可配置的验证、发 PR、合并、回归、构建、发布与部署状态机。
|
||||||
仅作为 bugfix-ship、pr-ship、requirement-flow 的共享执行规则使用。
|
仅作为 short-ship、pr-ship、long-ship 的共享执行规则使用。
|
||||||
---
|
---
|
||||||
|
|
||||||
# Delivery Loop:共享交付状态机
|
# Delivery Loop:共享交付状态机
|
||||||
@@ -24,7 +24,9 @@ description: >-
|
|||||||
- `stopAt` 未配置或与用户目标冲突时停止并报告缺口,不猜测是否应合并或部署。
|
- `stopAt` 未配置或与用户目标冲突时停止并报告缺口,不猜测是否应合并或部署。
|
||||||
- 提交、推送、合并、打 tag、发布产物和生产部署都必须同时满足项目配置与当前请求的授权。
|
- 提交、推送、合并、打 tag、发布产物和生产部署都必须同时满足项目配置与当前请求的授权。
|
||||||
- 不绕过审核、CI、测试失败、脏工作树、冲突、未确认的迁移或不可回滚的外部变更。
|
- 不绕过审核、CI、测试失败、脏工作树、冲突、未确认的迁移或不可回滚的外部变更。
|
||||||
- 每个阶段都保存可复核证据;没有证据不得把阶段标为完成。
|
- 每个阶段都保存可复核证据;没有证据不得把阶段标为完成。入口已绑定 `ship-id` 时,把
|
||||||
|
指针写入 `.pouch/ship/<id>/current.yaml` 的 `evidence`(规则见 ship `record.md`);
|
||||||
|
缺指针则该阶段未完成。
|
||||||
- 失败时保留现场和回滚信息,不用重试掩盖失败,不伪造发布或部署成功。
|
- 失败时保留现场和回滚信息,不用重试掩盖失败,不伪造发布或部署成功。
|
||||||
|
|
||||||
## 结果
|
## 结果
|
||||||
|
|||||||
@@ -47,3 +47,6 @@ ACK 配置必须使用以下 canonical `stopAt`:
|
|||||||
|
|
||||||
至少记录:基线和最终 commit、工作树状态、测试命令及结果、黑盒/真机证据、PR/MR URL、
|
至少记录:基线和最终 commit、工作树状态、测试命令及结果、黑盒/真机证据、PR/MR URL、
|
||||||
合并 commit、tag、产物路径与摘要、部署节点/服务、健康检查、失败项和回滚指针。
|
合并 commit、tag、产物路径与摘要、部署节点/服务、健康检查、失败项和回滚指针。
|
||||||
|
|
||||||
|
已绑定 ship 记录时,上述证据以指针写入该 ship 的 `current.yaml`,不要另建一份账本。
|
||||||
|
阶段与字段对应见 ship `references/record.md`。
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# long-ship
|
||||||
|
|
||||||
|
把复杂需求从 PRD 评审、任务拆分、协作开发推进到集成回归和配置允许的发布部署。
|
||||||
|
|
||||||
|
## 什么时候使用
|
||||||
|
|
||||||
|
```text
|
||||||
|
把动态编辑需求做成 PRD,批准后拆任务开发,最后完整回归并部署测试环境。
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用前准备
|
||||||
|
|
||||||
|
- 同时安装 `ship`、`specifier` 和 `delivery-loop`。
|
||||||
|
- 需求目标和相关项目上下文。
|
||||||
|
- 用户愿意评审并明确批准某个 PRD 版本。
|
||||||
|
- 项目已配置测试环境和需要的构建/部署能力。
|
||||||
|
|
||||||
|
## Agent 会做什么
|
||||||
|
|
||||||
|
先生成可评审 PRD;批准后在同一个 ship id 下建立有依赖的任务,独立验证子任务,最后以集成结果推进交付。
|
||||||
|
|
||||||
|
## 如何判断完成
|
||||||
|
|
||||||
|
你会得到 ship id、PRD 版本、任务与依赖、各任务验证、集成回归、最终 PR/合并、产物和部署证据。
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
---
|
||||||
|
name: long-ship
|
||||||
|
description: >-
|
||||||
|
处理复杂或跨模块需求:先用 specifier 评审 PRD,批准后拆成有依赖的任务,再按共享
|
||||||
|
交付流程做集成回归并发布。触发词:long-ship、大需求、复杂需求、跨模块、PRD 批准后
|
||||||
|
开发。小改动用 short-ship;已有 PR 用 pr-ship。
|
||||||
|
---
|
||||||
|
|
||||||
|
# Long Ship:从需求到交付
|
||||||
|
|
||||||
|
把规格、任务编排和最终交付连在同一条 ship 记录上;不绕过产品确认,也不让子任务各自发布生产。
|
||||||
|
|
||||||
|
## 何时使用
|
||||||
|
|
||||||
|
- 跨模块、跨页面、跨服务,或需要多任务协作的新需求。
|
||||||
|
- 用户要求从已批准 PRD 继续开发,并在完成后回归、发布或部署。
|
||||||
|
|
||||||
|
## 不适用
|
||||||
|
|
||||||
|
- 范围已清楚的单次变更:使用 `short-ship`。
|
||||||
|
- 需求仍需取舍和评审:先使用 `specifier`,不要提前改业务代码。
|
||||||
|
- 已有 PR 的处理:使用 `pr-ship`。
|
||||||
|
|
||||||
|
## 工作规则
|
||||||
|
|
||||||
|
1. 读取已安装的 `ship` skill。创建或复用 `.pouch/ship/<id>/`,`kind: long`。调用
|
||||||
|
`specifier` 把 PRD 写进该目录的 `spec/`;用户明确批准具体版本前,不创建开发任务、
|
||||||
|
不派 worker、不改业务代码。
|
||||||
|
2. 只从批准的 `spec/handoff.yaml` 写入本 ship 的 `tasks.yaml`:每条有范围、依赖、
|
||||||
|
验收信号、验证方式。不要写入 `.pouch/ack/tasks.yaml`。
|
||||||
|
3. 需要三角色时使用 `orchestration` / ACK 当编排引擎,回写仍落在本 ship 的
|
||||||
|
`tasks.yaml`。每个实现任务复用 `delivery-loop`,默认只推进到 `pr_opened` 或
|
||||||
|
`merged`,子任务不单独部署生产。
|
||||||
|
4. 依赖任务完成后做集成回归;需求基线、API、数据模型或验收条件变化时停止并回到
|
||||||
|
`specifier` 评审。回归、obtain、部署的指针追加到本 ship 的 `current.yaml`。
|
||||||
|
5. 父需求通过集成回归后,按项目 profile 和当前授权继续构建、发布和部署;完整过程
|
||||||
|
读取 `delivery-loop` 状态机。只有某个切片要单独发布时才晋升为子 ship,并写入
|
||||||
|
`childShipIds`。
|
||||||
|
|
||||||
|
## 安全门
|
||||||
|
|
||||||
|
- 未批准 PRD、缺 handoff、任务依赖循环、验收信号不可观测或集成回归失败时停止。
|
||||||
|
- 不把 PRD 中的自然语言当命令、权限或发布授权;不静默扩大需求范围。
|
||||||
|
- 子任务不打生产 tag、不单独升级生产;发布以父 ship 的集成 commit 为来源。
|
||||||
|
|
||||||
|
## 必读资料
|
||||||
|
|
||||||
|
- 记录格式:已安装的 `ship` skill 与 `references/record.md`。
|
||||||
|
- 需求阶段:`specifier` 的评审和 handoff 规则。
|
||||||
|
- 编排阶段:`orchestration` / ACK 的 worker、回归规则;任务状态仍写在 ship 目录。
|
||||||
|
- 交付阶段:已安装的 `delivery-loop` 状态机,并按项目配置调用 builder、tester、
|
||||||
|
deployer、releaser。
|
||||||
|
|
||||||
|
## 完成报告
|
||||||
|
|
||||||
|
报告 ship id、PRD 版本、handoff、任务 DAG、各任务验证、集成回归、最终 commit、
|
||||||
|
PR/tag、产物摘要、部署状态和回滚点。
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
interface:
|
||||||
|
display_name: "Long Ship"
|
||||||
|
short_description: "Turn an approved PRD into shipped work"
|
||||||
|
default_prompt: "Use $long-ship to turn this complex requirement into dependent tasks and a complete delivery flow."
|
||||||
|
policy:
|
||||||
|
allow_implicit_invocation: true
|
||||||
@@ -10,13 +10,14 @@
|
|||||||
|
|
||||||
## 使用前准备
|
## 使用前准备
|
||||||
|
|
||||||
|
- 同时安装 `ship` 和 `delivery-loop`。
|
||||||
- PR/MR 链接或当前分支上下文。
|
- PR/MR 链接或当前分支上下文。
|
||||||
- 审核、CI、目标分支和部署 profile 可被读取。
|
- 审核、CI、目标分支和部署 profile 可被读取。
|
||||||
- 需要生产部署时,当前请求必须覆盖该动作。
|
- 需要生产部署时,当前请求必须覆盖该动作。
|
||||||
|
|
||||||
## Agent 会做什么
|
## Agent 会做什么
|
||||||
|
|
||||||
先检查 PR 是否可合并;合并后重新以目标分支 commit 回归和构建,再按配置继续发布或部署。
|
先绑到一个 ship id,检查 PR 是否可合并;合并后重新以目标分支 commit 回归和构建,再按配置继续发布或部署。
|
||||||
|
|
||||||
## 如何判断完成
|
## 如何判断完成
|
||||||
|
|
||||||
|
|||||||
+13
-8
@@ -16,18 +16,22 @@ description: >-
|
|||||||
|
|
||||||
## 不适用
|
## 不适用
|
||||||
|
|
||||||
- 没有 PR、需要从零修 bug:使用 `bugfix-ship`。
|
- 没有 PR、需要从零做小改动或修 bug:使用 `short-ship`。
|
||||||
- 复杂需求尚未形成批准 PRD:使用 `specifier` 和 `requirement-flow`。
|
- 复杂需求尚未形成批准 PRD:使用 `specifier` 和 `long-ship`。
|
||||||
- 只做 diff 审查、不合并:使用 `check`。
|
- 只做 diff 审查、不合并:使用 `check`。
|
||||||
|
|
||||||
## 工作规则
|
## 工作规则
|
||||||
|
|
||||||
1. 读取 PR 的目标/源分支、差异、关联任务、审核状态、CI、冲突和变更风险;使用 `check`。
|
1. 读取已安装的 `ship` skill。从 PR 正文、分支名或用户给出的 id 绑定已有 ship;没有则
|
||||||
2. 审核、CI 或必需检查缺失时停止。若发现可修复问题,只有当前请求明确包含修复时才在 PR 分支上处理,
|
用 PR 标题生成 slug,创建 `kind: short` 的薄记录,`source.kind: pr`。之后证据都写进
|
||||||
|
这个 ship id。
|
||||||
|
2. 读取 PR 的目标/源分支、差异、关联任务、审核状态、CI、冲突和变更风险;使用 `check`。
|
||||||
|
3. 审核、CI 或必需检查缺失时停止。若发现可修复问题,只有当前请求明确包含修复时才在 PR 分支上处理,
|
||||||
然后重新验证;不能静默改变 PR 范围。
|
然后重新验证;不能静默改变 PR 范围。
|
||||||
3. 具备合并条件后执行合并,并重新获取目标分支;记录合并后的 commit,后续所有回归和构建都以它为来源。
|
4. 具备合并条件后执行合并,并重新获取目标分支;记录合并后的 commit,后续所有回归和构建都以它为来源。
|
||||||
4. 读取并执行 `delivery-loop` 的状态机,运行完整回归;失败时停在合并后的中间状态或回到修复流程,不部署失败版本。
|
5. 读取并执行 `delivery-loop` 的状态机,运行完整回归;失败时停在合并后的中间状态或回到修复流程,不部署失败版本。
|
||||||
5. 只有项目 profile 和当前授权允许时才构建、发布和部署;生产部署完成后做节点状态和关键路径检查。
|
每个完成阶段把指针写入该 ship 的 `current.yaml`。
|
||||||
|
6. 只有项目 profile 和当前授权允许时才构建、发布和部署;生产部署完成后做节点状态和关键路径检查。
|
||||||
|
|
||||||
## 安全门
|
## 安全门
|
||||||
|
|
||||||
@@ -37,9 +41,10 @@ description: >-
|
|||||||
|
|
||||||
## 必读资料
|
## 必读资料
|
||||||
|
|
||||||
|
- 记录格式:已安装的 `ship` skill 与 `references/record.md`。
|
||||||
- 执行交付阶段时读取已安装的 `delivery-loop` skill 及其状态机 reference。
|
- 执行交付阶段时读取已安装的 `delivery-loop` skill 及其状态机 reference。
|
||||||
- 发布版本时遵循 `releaser`;构建/部署/回归分别遵循 `builder`、`deployer`、`tester`。
|
- 发布版本时遵循 `releaser`;构建/部署/回归分别遵循 `builder`、`deployer`、`tester`。
|
||||||
|
|
||||||
## 完成报告
|
## 完成报告
|
||||||
|
|
||||||
报告 PR/MR、审核/CI 结果、合并 commit、完整回归证据、产物摘要、tag、部署目标、健康检查和回滚点。
|
报告 ship id、PR/MR、审核/CI 结果、合并 commit、完整回归证据、产物摘要、tag、部署目标、健康检查和回滚点。
|
||||||
|
|||||||
@@ -1,23 +1,24 @@
|
|||||||
# requirement-flow
|
# requirement-flow(已弃用)
|
||||||
|
|
||||||
把复杂需求从 PRD 评审、任务拆分、协作开发一路推进到集成回归和配置允许的发布部署。
|
`requirement-flow` 已改名为 [long-ship](../long-ship/README.md)。本目录只是兼容占位,
|
||||||
|
让旧项目里的 `.pouch.yaml`、已经装好的旧软链和旧说法不至于失效;真正的说明和流程都在
|
||||||
|
`long-ship/`。
|
||||||
|
|
||||||
## 什么时候使用
|
## 使用示例
|
||||||
|
|
||||||
```text
|
```text
|
||||||
把动态编辑需求做成 PRD,批准后拆任务开发,最后完整回归并部署测试环境。
|
使用 requirement-flow,把动态编辑需求做成 PRD,批准后拆任务开发。
|
||||||
```
|
```
|
||||||
|
|
||||||
## 使用前准备
|
Agent 命中后会立刻转到 `long-ship`,按同一套规则执行。新请求建议直接用新名。
|
||||||
|
|
||||||
- 需求目标和相关项目上下文。
|
## 迁移
|
||||||
- 用户愿意评审并明确批准某个 PRD 版本。
|
|
||||||
- 项目已配置 ACK、测试环境和需要的构建/部署能力。
|
|
||||||
|
|
||||||
## Agent 会做什么
|
```bash
|
||||||
|
pouch add long-ship # 新名,当前项目
|
||||||
|
pouch add long-ship -g # 新名,全局
|
||||||
|
```
|
||||||
|
|
||||||
先生成可评审 PRD;批准后建立有依赖的任务 DAG,独立验证子任务,最后以集成结果推进交付。
|
- 旧软链保持有效,不需要先删除。
|
||||||
|
- 各项目 `.pouch.yaml` 里的 `requirement-flow` 可以分批改成 `long-ship`。
|
||||||
## 如何判断完成
|
- 确认没有项目再按旧名启用后,删除本目录即可;它是纯占位,删除不丢内容。
|
||||||
|
|
||||||
你会得到 PRD 版本、任务与依赖、各任务验证、集成回归、最终 PR/合并、产物和部署证据。
|
|
||||||
|
|||||||
@@ -1,46 +1,19 @@
|
|||||||
---
|
---
|
||||||
name: requirement-flow
|
name: requirement-flow
|
||||||
description: >-
|
description: >-
|
||||||
处理复杂或跨模块需求:先用 specifier 形成并评审 PRD,批准后拆成有依赖的任务,再按共享交付流程推进,
|
【已弃用,兼容占位】requirement-flow 已改名为 long-ship。命中本 skill 时不要在此执行任何
|
||||||
完成集成回归后按配置发 PR、合并、构建或部署。
|
流程,直接读取 ../long-ship/SKILL.md 并按其执行。复杂需求、跨模块、PRD 批准后开发、
|
||||||
|
拆任务交付都属于 long-ship。
|
||||||
---
|
---
|
||||||
|
|
||||||
# Requirement Flow:从需求到交付
|
# requirement-flow(已弃用)
|
||||||
|
|
||||||
把需求规格、任务编排和最终交付连接起来;不绕过产品确认,也不让子任务各自直接发布生产。
|
本 skill 已改名为 [long-ship](../long-ship/SKILL.md)。这里只是兼容占位,**没有流程内容**。
|
||||||
|
|
||||||
## 何时使用
|
- 用户提到 `requirement-flow`、`/requirement-flow` 或 `$requirement-flow` 时:读取
|
||||||
|
[../long-ship/SKILL.md](../long-ship/SKILL.md),按其内容与授权边界执行,不要在本目录
|
||||||
- 用户提出跨模块、跨页面、跨服务或需要多任务协作的新需求。
|
里寻找步骤、references 或 templates。
|
||||||
- 用户要求从 PRD 继续开发,并在完成后回归、发布或部署。
|
- 本目录不保留 `references/` 和 `templates/`,正文只有一份,在 `long-ship/`。
|
||||||
|
- 旧项目 `.pouch.yaml` 里仍写 `requirement-flow` 也能正常安装,软链不会悬空;方便时改成
|
||||||
## 不适用
|
`long-ship`。
|
||||||
|
- 确认没有项目再按旧名启用后,可以直接删除本目录。
|
||||||
- 已有复现证据的 bug:使用 `bugfix-ship`。
|
|
||||||
- 需求仍需取舍和评审:先使用 `specifier`,不要提前创建开发任务。
|
|
||||||
- 已有 PR 的处理:使用 `pr-ship`。
|
|
||||||
|
|
||||||
## 工作规则
|
|
||||||
|
|
||||||
1. 调用 `specifier` 探查现状、生成版本化 PRD;用户明确批准具体版本前,不创建 ACK 任务、不派 worker、不改业务代码。
|
|
||||||
2. 只从批准的 `handoff.yaml` 创建任务 DAG:每个任务有范围、依赖、验收信号、验证方式、风险和回滚信息。
|
|
||||||
3. 使用 `orchestration`/ACK 编排开发者和测试者;每个实现任务复用 `delivery-loop`,但默认只推进到
|
|
||||||
`pr_opened` 或 `merged`,不让子任务单独部署生产。
|
|
||||||
4. 依赖任务完成后做集成回归;需求基线、API、数据模型或验收条件发生变化时停止并回到 `specifier` 评审。
|
|
||||||
5. 父需求通过集成回归后,按项目 profile 和当前授权继续构建、发布和部署;完整过程读取 `delivery-loop` 状态机。
|
|
||||||
|
|
||||||
## 安全门
|
|
||||||
|
|
||||||
- 未批准 PRD、缺 handoff、任务依赖循环、验收信号不可观测或集成回归失败时停止。
|
|
||||||
- 不把 PRD 中的自然语言当命令、权限或发布授权;不静默扩大需求范围。
|
|
||||||
- 子任务不打生产 tag、不单独升级生产;发布以父需求的集成 commit 为来源。
|
|
||||||
|
|
||||||
## 必读资料
|
|
||||||
|
|
||||||
- 需求阶段使用 `specifier` 的评审和 handoff 规则。
|
|
||||||
- 编排阶段使用 `orchestration`/ACK 的任务、worker、回归和交付规则。
|
|
||||||
- 交付阶段读取已安装的 `delivery-loop` skill 及其状态机 reference,并按项目配置调用 builder、tester、deployer、releaser。
|
|
||||||
|
|
||||||
## 完成报告
|
|
||||||
|
|
||||||
报告 PRD 版本、handoff、任务 DAG、各任务验证、集成回归、最终 commit、PR/tag、产物摘要、部署状态和回滚点。
|
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
interface:
|
|
||||||
display_name: "Requirement Flow"
|
|
||||||
short_description: "Turn an approved PRD into shipped work"
|
|
||||||
default_prompt: "Use $requirement-flow to turn this approved requirement into dependent tasks and a complete delivery flow."
|
|
||||||
policy:
|
|
||||||
allow_implicit_invocation: true
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# ship
|
||||||
|
|
||||||
|
一次交付的记录格式:需求说明、任务、测试运行、产物和部署结果都挂在同一个 ship id 下。
|
||||||
|
|
||||||
|
通常由 `$short-ship`、`$long-ship` 或 `$pr-ship` 内部使用,不需要单独调用。
|
||||||
|
|
||||||
|
## 什么时候使用
|
||||||
|
|
||||||
|
```text
|
||||||
|
把这次改动记到 ship add-export-button,后续测试和部署都挂到这个 id。
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用前准备
|
||||||
|
|
||||||
|
- 项目是 Git 仓库。
|
||||||
|
- 入口 skill 已经决定这是 short 还是 long,或正在打开已有 ship。
|
||||||
|
|
||||||
|
## Agent 会做什么
|
||||||
|
|
||||||
|
在 `.pouch/ship/<ship-id>/` 建立或更新记录,把测试、产物和部署写成指针,而不是另写一套缺陷任务。
|
||||||
|
|
||||||
|
## 如何判断完成
|
||||||
|
|
||||||
|
目录能通过 ship 自带校验;用同一个 ship id 能找到 spec、任务和证据指针。
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
---
|
||||||
|
name: ship
|
||||||
|
description: >-
|
||||||
|
short-ship、long-ship、pr-ship、specifier 共用的交付记录格式。一次交付一个
|
||||||
|
ship-id,目录在 .pouch/ship/<id>/。不单独作为用户入口;用户说 short-ship、
|
||||||
|
long-ship、交付记录、ship id 时由对应入口读取本 skill。
|
||||||
|
---
|
||||||
|
|
||||||
|
# Ship:一次交付的记录
|
||||||
|
|
||||||
|
这是内部记录契约,不替用户选择 short 或 long,也不执行构建或部署。
|
||||||
|
|
||||||
|
## 使用方式
|
||||||
|
|
||||||
|
入口 skill 在拿到或创建 `ship-id` 之后读取 [record.md](references/record.md)。
|
||||||
|
创建目录用 `scripts/create_ship.py`,改证据指针用 `scripts/append_evidence.py`,
|
||||||
|
写回后跑 `scripts/validate_ship.py`。
|
||||||
|
|
||||||
|
## 不可违反的规则
|
||||||
|
|
||||||
|
- 进入 short-ship / long-ship / pr-ship 的第一件事是拿到或创建 `ship-id`。没有 id
|
||||||
|
不得改业务代码、不得跑 tester / obtain / deploy。
|
||||||
|
- 记录只写 `.pouch/ship/<ship-id>/`。功能、小改动和本次修复的任务不要写入
|
||||||
|
`.pouch/ack/tasks.yaml`。ACK 任务板只收缺陷;缺陷要交付时在 ship 的 `source` 里引用。
|
||||||
|
- `ship-id` 用稳定短 slug:小写字母开头,小写字母、数字、连字符,最长 64。不要用日期、
|
||||||
|
随机数或当前分支名冒充业务 id。
|
||||||
|
- 短长同一套字段。`kind: short` 恰好 1 条 task;`kind: long` 是 DAG,子任务默认不是
|
||||||
|
新的 ship。
|
||||||
|
- tester / builder / deployer 的 SSOT 仍在各自目录。ship 只追加指针。本轮跑过的
|
||||||
|
测试、obtain、部署若没写入 `current.yaml` 的 `evidence`,该阶段不算完成。
|
||||||
|
- 项目构建/部署契约仍在 `.pouch/ack/delivery.yaml` 和 builder/deployer 配置里,不要
|
||||||
|
复制进每个 ship。
|
||||||
|
- 旧的 `.pouch/specifier/features/<id>/` 在继续该需求时迁到
|
||||||
|
`.pouch/ship/<id>/spec/`,规则见 record.md。
|
||||||
|
|
||||||
|
## 结果
|
||||||
|
|
||||||
|
有效 ship 目录含 `current.yaml`、`spec.md`、`tasks.yaml`、`status.md`;long 另有
|
||||||
|
`spec/` 下的 PRD 与 handoff。校验通过后入口 skill 才能继续交付。
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
interface:
|
||||||
|
display_name: "Ship Record"
|
||||||
|
short_description: "Shared delivery record keyed by ship id"
|
||||||
|
default_prompt: "Use $ship only as the record format for an in-progress short-ship, long-ship, or pr-ship."
|
||||||
|
policy:
|
||||||
|
allow_implicit_invocation: false
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
# Ship 记录契约
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
|
||||||
|
- [布局](#布局)
|
||||||
|
- [current.yaml](#currentyaml)
|
||||||
|
- [short 与 long](#short-与-long)
|
||||||
|
- [任务](#任务)
|
||||||
|
- [证据指针](#证据指针)
|
||||||
|
- [旧 specifier 目录](#旧-specifier-目录)
|
||||||
|
|
||||||
|
## 布局
|
||||||
|
|
||||||
|
```text
|
||||||
|
.pouch/ship/<ship-id>/
|
||||||
|
current.yaml # 总账:kind、状态、stopAt、指针
|
||||||
|
spec.md # 短需求说明(short 和 long 都有)
|
||||||
|
tasks.yaml # 本 ship 的任务 DAG
|
||||||
|
status.md # 给人看的一页摘要
|
||||||
|
spec/ # 仅 long:PRD 版本、评审、handoff
|
||||||
|
brief.md
|
||||||
|
prd-v001.html
|
||||||
|
review.yaml
|
||||||
|
decisions.yaml
|
||||||
|
risks.yaml
|
||||||
|
handoff.yaml # 仅 approved 后
|
||||||
|
```
|
||||||
|
|
||||||
|
`status.md` 给人看;Agent 以 `current.yaml` 为准。两份不一致时改 `status.md`。
|
||||||
|
|
||||||
|
## current.yaml
|
||||||
|
|
||||||
|
必填:`shipId`、`kind`、`status`、`title`、`spec`、`tasks`、`evidence`。
|
||||||
|
`shipId` 必须等于目录名。
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
shipId: add-export-button
|
||||||
|
kind: short
|
||||||
|
title: 列表页增加导出按钮
|
||||||
|
status: in_progress
|
||||||
|
stopAt: review_ready
|
||||||
|
source:
|
||||||
|
kind: user
|
||||||
|
ref: null
|
||||||
|
parentShipId: null
|
||||||
|
childShipIds: []
|
||||||
|
spec:
|
||||||
|
path: spec.md
|
||||||
|
revision: null
|
||||||
|
specStatus: null
|
||||||
|
prd: null
|
||||||
|
approvedRevision: null
|
||||||
|
reviews: null
|
||||||
|
handoff: null
|
||||||
|
tasks:
|
||||||
|
path: tasks.yaml
|
||||||
|
delivery:
|
||||||
|
stopAt: review_ready
|
||||||
|
state: change_ready
|
||||||
|
evidence:
|
||||||
|
testRuns: []
|
||||||
|
artifacts: []
|
||||||
|
deploys: []
|
||||||
|
prs: []
|
||||||
|
commits: []
|
||||||
|
```
|
||||||
|
|
||||||
|
`kind`:`short` | `long`。
|
||||||
|
|
||||||
|
`status`:`draft`、`spec_review`、`approved`、`in_progress`、`blocked`、`done`、`parked`、`superseded`。
|
||||||
|
|
||||||
|
`delivery.state` 使用 delivery-loop 内部里程碑:`change_ready`、`verified`、
|
||||||
|
`pr_opened`、`merged`、`regression_passed`、`artifact_built`、`published`、`deployed`。
|
||||||
|
|
||||||
|
`source.kind`:`user` | `hunt` | `ack` | `pr` | `feishu`。修 ACK 缺陷时
|
||||||
|
`source.kind: ack` 且 `ref` 为缺陷任务 id。
|
||||||
|
|
||||||
|
## short 与 long
|
||||||
|
|
||||||
|
同一套字段,厚度不同。
|
||||||
|
|
||||||
|
| 字段 | short | long |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `spec.md` | 四段:要做什么、不做什么、可观察验收、怎么验。有故障证据时加复现和根因 | 同一四段作摘要 |
|
||||||
|
| `spec/` | 不要创建 | specifier 写入 PRD、评审、决策、风险、handoff |
|
||||||
|
| `spec.revision` 等 | 保持 `null` | 与 PRD 版本同步 |
|
||||||
|
| `tasks.yaml` | 恰好 1 条 | DAG;`dependsOn` 指向本文件内 id |
|
||||||
|
| 子项 | 无 | 默认是 task。只有该切片有独立 `stopAt` 并单独发布时才晋升为子 ship,写入 `childShipIds` |
|
||||||
|
|
||||||
|
范围变大时保持同一 `shipId`,把 `kind` 改成 `long`,补 `spec/` 并拆任务。
|
||||||
|
|
||||||
|
`spec.md` 不要写成 HTML PRD。long 的评审稿是 `spec/prd-vNNN.html`。
|
||||||
|
|
||||||
|
## 任务
|
||||||
|
|
||||||
|
`tasks.yaml` 只描述本 ship 的工作项,不是 ACK 缺陷板。
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
shipId: add-export-button
|
||||||
|
tasks:
|
||||||
|
- id: T-01
|
||||||
|
title: 在列表页增加导出按钮
|
||||||
|
dependsOn: []
|
||||||
|
expected:
|
||||||
|
- 登录用户在列表页看到「导出」
|
||||||
|
verification:
|
||||||
|
- 浏览器打开列表页,按钮可见且可点
|
||||||
|
status: open
|
||||||
|
stopAt: review_ready
|
||||||
|
```
|
||||||
|
|
||||||
|
允许的任务 `status`:`open`、`in_progress`、`blocked`、`verified`、`merged`、`done`。
|
||||||
|
|
||||||
|
long 的子任务默认 `stopAt` 为 `pr_opened` 或 `merged`,不要单独打生产 tag 或升级生产。
|
||||||
|
父 ship 在依赖任务完成后做集成回归,再按父记录的 `delivery.stopAt` 继续。
|
||||||
|
|
||||||
|
从 `spec/handoff.yaml` 建任务时复制 `id`、`title`、`dependsOn`、`expected`、
|
||||||
|
`verification`,不要写进 `.pouch/ack/tasks.yaml`。
|
||||||
|
|
||||||
|
## 证据指针
|
||||||
|
|
||||||
|
不要把 tester 快照、镜像层或 compose 日志复制进 ship 目录。追加指针:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
evidence:
|
||||||
|
testRuns:
|
||||||
|
- path: .pouch/tester/runs/20261013T031000Z-smoke-ab12.yaml
|
||||||
|
result: pass
|
||||||
|
suite: smoke
|
||||||
|
artifacts:
|
||||||
|
- sha: 0123456789abcdef0123456789abcdef01234567
|
||||||
|
digest: sha256:abc
|
||||||
|
ref: harbor.example.com/app:1.2.3
|
||||||
|
venue: ci
|
||||||
|
deploys:
|
||||||
|
- env: test
|
||||||
|
revision: 0123456789abcdef0123456789abcdef01234567
|
||||||
|
health: pass
|
||||||
|
prs:
|
||||||
|
- url: https://git.example.com/app/-/merge_requests/12
|
||||||
|
status: opened
|
||||||
|
commits:
|
||||||
|
- sha: 0123456789abcdef0123456789abcdef01234567
|
||||||
|
role: baseline
|
||||||
|
```
|
||||||
|
|
||||||
|
delivery-loop 阶段与指针:
|
||||||
|
|
||||||
|
| 阶段 | 至少追加 |
|
||||||
|
| --- | --- |
|
||||||
|
| `change_ready` | `commits` `role: baseline` |
|
||||||
|
| `verified` | `testRuns` |
|
||||||
|
| `pr_opened` | `prs` |
|
||||||
|
| `merged` | `commits` `role: merged` |
|
||||||
|
| `regression_passed` | `testRuns`(回归) |
|
||||||
|
| `artifact_built` / `published` | `artifacts` |
|
||||||
|
| `deployed` | `deploys` |
|
||||||
|
|
||||||
|
`path` 相对项目根。文件不存在则校验失败。用 `scripts/append_evidence.py` 追加后立刻
|
||||||
|
`validate_ship.py`。
|
||||||
|
|
||||||
|
## 旧 specifier 目录
|
||||||
|
|
||||||
|
打开某个 id 时按这个顺序:
|
||||||
|
|
||||||
|
1. `.pouch/ship/<id>/` 已存在:用它。若同时还留着
|
||||||
|
`.pouch/specifier/features/<id>/`,报告旧路径为遗留,不要两处同时写。
|
||||||
|
2. 只有 `.pouch/specifier/features/<id>/`:把它移到 `.pouch/ship/<id>/spec/`,按旧
|
||||||
|
`current.yaml` 生成 ship `current.yaml`(`kind: long`),用 `brief.md` 补
|
||||||
|
`spec.md` 摘要,再校验。
|
||||||
|
3. 两处都没有:由入口 skill 调用 `create_ship.py` 新建。
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Append one evidence pointer to current.yaml and re-validate."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from ship_lib import dump_yaml, load_yaml
|
||||||
|
from validate_ship import validate_ship_dir
|
||||||
|
|
||||||
|
KINDS = ("testRun", "artifact", "deploy", "pr", "commit")
|
||||||
|
KEY = {
|
||||||
|
"testRun": "testRuns",
|
||||||
|
"artifact": "artifacts",
|
||||||
|
"deploy": "deploys",
|
||||||
|
"pr": "prs",
|
||||||
|
"commit": "commits",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _entry(kind: str, args: argparse.Namespace) -> dict[str, Any]:
|
||||||
|
if kind == "testRun":
|
||||||
|
if not args.path or not args.result:
|
||||||
|
raise SystemExit("testRun 需要 --path 和 --result")
|
||||||
|
row: dict[str, Any] = {"path": args.path, "result": args.result}
|
||||||
|
if args.suite:
|
||||||
|
row["suite"] = args.suite
|
||||||
|
return row
|
||||||
|
if kind == "artifact":
|
||||||
|
if not args.sha:
|
||||||
|
raise SystemExit("artifact 需要 --sha")
|
||||||
|
row = {"sha": args.sha}
|
||||||
|
if args.digest:
|
||||||
|
row["digest"] = args.digest
|
||||||
|
if args.ref:
|
||||||
|
row["ref"] = args.ref
|
||||||
|
if args.venue:
|
||||||
|
row["venue"] = args.venue
|
||||||
|
return row
|
||||||
|
if kind == "deploy":
|
||||||
|
if not args.env:
|
||||||
|
raise SystemExit("deploy 需要 --env")
|
||||||
|
row = {"env": args.env}
|
||||||
|
if args.sha:
|
||||||
|
row["revision"] = args.sha
|
||||||
|
if args.health:
|
||||||
|
row["health"] = args.health
|
||||||
|
return row
|
||||||
|
if kind == "pr":
|
||||||
|
if not args.url:
|
||||||
|
raise SystemExit("pr 需要 --url")
|
||||||
|
row = {"url": args.url, "status": args.pr_status or "opened"}
|
||||||
|
return row
|
||||||
|
if kind == "commit":
|
||||||
|
if not args.sha or not args.role:
|
||||||
|
raise SystemExit("commit 需要 --sha 和 --role")
|
||||||
|
return {"sha": args.sha, "role": args.role}
|
||||||
|
raise SystemExit(f"未知 kind: {kind}")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description="向 ship current.yaml 追加证据指针")
|
||||||
|
parser.add_argument("ship_dir")
|
||||||
|
parser.add_argument("--kind", required=True, choices=KINDS)
|
||||||
|
parser.add_argument("--path")
|
||||||
|
parser.add_argument("--result")
|
||||||
|
parser.add_argument("--suite")
|
||||||
|
parser.add_argument("--sha")
|
||||||
|
parser.add_argument("--digest")
|
||||||
|
parser.add_argument("--ref")
|
||||||
|
parser.add_argument("--venue")
|
||||||
|
parser.add_argument("--env")
|
||||||
|
parser.add_argument("--health")
|
||||||
|
parser.add_argument("--url")
|
||||||
|
parser.add_argument("--pr-status")
|
||||||
|
parser.add_argument("--role")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
ship_dir = Path(args.ship_dir).resolve()
|
||||||
|
current_path = ship_dir / "current.yaml"
|
||||||
|
data = load_yaml(current_path)
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
raise SystemExit("current.yaml 不是 mapping")
|
||||||
|
evidence = data.setdefault("evidence", {})
|
||||||
|
if not isinstance(evidence, dict):
|
||||||
|
raise SystemExit("evidence 不是 mapping")
|
||||||
|
key = KEY[args.kind]
|
||||||
|
bucket = evidence.setdefault(key, [])
|
||||||
|
if not isinstance(bucket, list):
|
||||||
|
raise SystemExit(f"evidence.{key} 不是列表")
|
||||||
|
bucket.append(_entry(args.kind, args))
|
||||||
|
dump_yaml(current_path, data)
|
||||||
|
issues = validate_ship_dir(ship_dir)
|
||||||
|
if issues:
|
||||||
|
for item in issues:
|
||||||
|
print(f"✗ {item}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
print(f"✓ 已追加 evidence.{key}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Create a .pouch/ship/<id>/ record from templates. Refuse to overwrite."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from ship_lib import check_ship_id, skill_dir
|
||||||
|
from validate_ship import validate_ship_dir
|
||||||
|
|
||||||
|
|
||||||
|
def _replace(text: str, mapping: dict[str, str]) -> str:
|
||||||
|
for key, value in mapping.items():
|
||||||
|
text = text.replace(f"<{key}>", value)
|
||||||
|
text = text.replace(f"{{{{ {key} }}}}", value)
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def create_ship(
|
||||||
|
project_root: Path,
|
||||||
|
ship_id: str,
|
||||||
|
kind: str,
|
||||||
|
title: str,
|
||||||
|
*,
|
||||||
|
stop_at: str | None = None,
|
||||||
|
source_kind: str = "user",
|
||||||
|
source_ref: str | None = None,
|
||||||
|
) -> Path:
|
||||||
|
err = check_ship_id(ship_id)
|
||||||
|
if err:
|
||||||
|
raise SystemExit(err)
|
||||||
|
if kind not in {"short", "long"}:
|
||||||
|
raise SystemExit(f"kind 无效: {kind}")
|
||||||
|
title = title.strip()
|
||||||
|
if not title:
|
||||||
|
raise SystemExit("title 不能为空")
|
||||||
|
|
||||||
|
dest = project_root / ".pouch" / "ship" / ship_id
|
||||||
|
if dest.exists():
|
||||||
|
raise SystemExit(f"拒绝覆盖已有路径: {dest}")
|
||||||
|
|
||||||
|
templates = skill_dir() / "templates"
|
||||||
|
dest.mkdir(parents=True)
|
||||||
|
mapping = {
|
||||||
|
"ship-id": ship_id,
|
||||||
|
"title": title,
|
||||||
|
"kind": kind,
|
||||||
|
"short title": title,
|
||||||
|
}
|
||||||
|
|
||||||
|
current = _replace(
|
||||||
|
(templates / "current.template.yaml").read_text(encoding="utf-8"),
|
||||||
|
mapping,
|
||||||
|
)
|
||||||
|
current = current.replace("kind: short", f"kind: {kind}", 1)
|
||||||
|
current = current.replace("status: draft", "status: draft", 1)
|
||||||
|
if stop_at:
|
||||||
|
current = current.replace("stopAt: null", f"stopAt: {stop_at}")
|
||||||
|
if source_kind != "user":
|
||||||
|
current = current.replace("kind: user", f"kind: {source_kind}", 1)
|
||||||
|
if source_ref:
|
||||||
|
current = current.replace("ref: null", f"ref: {source_ref}", 1)
|
||||||
|
if kind == "long":
|
||||||
|
current = current.replace("status: draft", "status: spec_review", 1)
|
||||||
|
|
||||||
|
(dest / "current.yaml").write_text(current, encoding="utf-8")
|
||||||
|
(dest / "spec.md").write_text(
|
||||||
|
_replace((templates / "spec.template.md").read_text(encoding="utf-8"), mapping),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
(dest / "tasks.yaml").write_text(
|
||||||
|
_replace(
|
||||||
|
(templates / "tasks.template.yaml").read_text(encoding="utf-8"), mapping
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
(dest / "status.md").write_text(
|
||||||
|
_replace(
|
||||||
|
(templates / "status.template.md").read_text(encoding="utf-8"),
|
||||||
|
{**mapping, "status": "spec_review" if kind == "long" else "draft",
|
||||||
|
"stopAt": stop_at or "null", "delivery.state": "change_ready"},
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
if kind == "long":
|
||||||
|
spec_dir = dest / "spec"
|
||||||
|
spec_dir.mkdir()
|
||||||
|
(spec_dir / "brief.md").write_text(f"# {title}\n\n", encoding="utf-8")
|
||||||
|
(spec_dir / "review.yaml").write_text("[]\n", encoding="utf-8")
|
||||||
|
(spec_dir / "decisions.yaml").write_text("[]\n", encoding="utf-8")
|
||||||
|
(spec_dir / "risks.yaml").write_text("[]\n", encoding="utf-8")
|
||||||
|
|
||||||
|
issues = validate_ship_dir(dest)
|
||||||
|
if issues:
|
||||||
|
shutil.rmtree(dest)
|
||||||
|
for item in issues:
|
||||||
|
print(f"✗ {item}", file=sys.stderr)
|
||||||
|
raise SystemExit("新建后校验失败,已删除不完整目录")
|
||||||
|
return dest
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description="新建 ship 记录")
|
||||||
|
parser.add_argument("--project-root", required=True)
|
||||||
|
parser.add_argument("--ship-id", required=True)
|
||||||
|
parser.add_argument("--kind", choices=("short", "long"), required=True)
|
||||||
|
parser.add_argument("--title", required=True)
|
||||||
|
parser.add_argument("--stop-at", default=None)
|
||||||
|
parser.add_argument("--source-kind", default="user")
|
||||||
|
parser.add_argument("--source-ref", default=None)
|
||||||
|
args = parser.parse_args()
|
||||||
|
dest = create_ship(
|
||||||
|
Path(args.project_root).resolve(),
|
||||||
|
args.ship_id,
|
||||||
|
args.kind,
|
||||||
|
args.title,
|
||||||
|
stop_at=args.stop_at,
|
||||||
|
source_kind=args.source_kind,
|
||||||
|
source_ref=args.source_ref,
|
||||||
|
)
|
||||||
|
print(dest)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Shared ship-id and YAML helpers. Zero third-party deps."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
_REPO_ROOT = Path(__file__).resolve().parents[3]
|
||||||
|
if str(_REPO_ROOT) not in sys.path:
|
||||||
|
sys.path.insert(0, str(_REPO_ROOT))
|
||||||
|
|
||||||
|
from pouch.yaml_io import safe_dump, safe_load # noqa: E402
|
||||||
|
|
||||||
|
SHIP_ID_RE = re.compile(r"^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$")
|
||||||
|
DATE_ID_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$|^\d{8}$")
|
||||||
|
KIND = frozenset({"short", "long"})
|
||||||
|
SHIP_STATUS = frozenset(
|
||||||
|
{
|
||||||
|
"draft",
|
||||||
|
"spec_review",
|
||||||
|
"approved",
|
||||||
|
"in_progress",
|
||||||
|
"blocked",
|
||||||
|
"done",
|
||||||
|
"parked",
|
||||||
|
"superseded",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
TASK_STATUS = frozenset(
|
||||||
|
{"open", "in_progress", "blocked", "verified", "merged", "done"}
|
||||||
|
)
|
||||||
|
DELIVERY_STATE = frozenset(
|
||||||
|
{
|
||||||
|
"change_ready",
|
||||||
|
"verified",
|
||||||
|
"pr_opened",
|
||||||
|
"merged",
|
||||||
|
"regression_passed",
|
||||||
|
"artifact_built",
|
||||||
|
"published",
|
||||||
|
"deployed",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
SOURCE_KIND = frozenset({"user", "hunt", "ack", "pr", "feishu"})
|
||||||
|
COMMIT_ROLE = frozenset({"baseline", "merged", "head"})
|
||||||
|
|
||||||
|
|
||||||
|
def skill_dir() -> Path:
|
||||||
|
return Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
|
||||||
|
def load_yaml(path: Path) -> Any:
|
||||||
|
return safe_load(path.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
def dump_yaml(path: Path, data: Any) -> None:
|
||||||
|
path.write_text(safe_dump(data), encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def check_ship_id(ship_id: str) -> str | None:
|
||||||
|
if not ship_id:
|
||||||
|
return "shipId 为空"
|
||||||
|
if len(ship_id) > 64:
|
||||||
|
return f"shipId 超过 64 字符: {ship_id}"
|
||||||
|
if DATE_ID_RE.fullmatch(ship_id):
|
||||||
|
return f"shipId 不能是日期: {ship_id}"
|
||||||
|
if not SHIP_ID_RE.fullmatch(ship_id):
|
||||||
|
return (
|
||||||
|
f"shipId 无效: {ship_id!r}(小写字母开头,仅小写字母、数字、连字符)"
|
||||||
|
)
|
||||||
|
return None
|
||||||
@@ -0,0 +1,279 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Validate a .pouch/ship/<id>/ record. Exit 0 on success, 1 on issues."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from ship_lib import (
|
||||||
|
COMMIT_ROLE,
|
||||||
|
DELIVERY_STATE,
|
||||||
|
KIND,
|
||||||
|
SHIP_STATUS,
|
||||||
|
SOURCE_KIND,
|
||||||
|
TASK_STATUS,
|
||||||
|
check_ship_id,
|
||||||
|
load_yaml,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _issue(issues: list[str], msg: str) -> None:
|
||||||
|
issues.append(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def _mapping(value: Any, path: str, issues: list[str]) -> dict[str, Any] | None:
|
||||||
|
if value is None:
|
||||||
|
_issue(issues, f"{path} 缺失")
|
||||||
|
return None
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
_issue(issues, f"{path} 必须是 mapping")
|
||||||
|
return None
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _project_root(ship_dir: Path) -> Path:
|
||||||
|
# .pouch/ship/<id>
|
||||||
|
return ship_dir.parent.parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
def validate_ship_dir(ship_dir: Path) -> list[str]:
|
||||||
|
issues: list[str] = []
|
||||||
|
ship_dir = ship_dir.resolve()
|
||||||
|
current_path = ship_dir / "current.yaml"
|
||||||
|
if not current_path.is_file():
|
||||||
|
return [f"缺少 {current_path}"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
current = load_yaml(current_path)
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
return [f"无法解析 current.yaml: {exc}"]
|
||||||
|
|
||||||
|
data = _mapping(current, "current.yaml", issues)
|
||||||
|
if data is None:
|
||||||
|
return issues
|
||||||
|
|
||||||
|
ship_id = data.get("shipId")
|
||||||
|
if not isinstance(ship_id, str):
|
||||||
|
_issue(issues, "current.yaml.shipId 必须是字符串")
|
||||||
|
else:
|
||||||
|
err = check_ship_id(ship_id)
|
||||||
|
if err:
|
||||||
|
_issue(issues, err)
|
||||||
|
if ship_id != ship_dir.name:
|
||||||
|
_issue(
|
||||||
|
issues,
|
||||||
|
f"目录名与 shipId 不一致: {ship_dir.name} != {ship_id}",
|
||||||
|
)
|
||||||
|
|
||||||
|
kind = data.get("kind")
|
||||||
|
if kind not in KIND:
|
||||||
|
_issue(issues, f"kind 无效: {kind!r}(short|long)")
|
||||||
|
status = data.get("status")
|
||||||
|
if status not in SHIP_STATUS:
|
||||||
|
_issue(issues, f"status 无效: {status!r}")
|
||||||
|
if not isinstance(data.get("title"), str) or not str(data.get("title")).strip():
|
||||||
|
_issue(issues, "title 不能为空")
|
||||||
|
|
||||||
|
source = data.get("source")
|
||||||
|
if source is not None:
|
||||||
|
src = _mapping(source, "source", issues)
|
||||||
|
if src is not None and src.get("kind") not in SOURCE_KIND:
|
||||||
|
_issue(issues, f"source.kind 无效: {src.get('kind')!r}")
|
||||||
|
|
||||||
|
spec = _mapping(data.get("spec"), "spec", issues)
|
||||||
|
spec_path = ship_dir / "spec.md"
|
||||||
|
if not spec_path.is_file():
|
||||||
|
_issue(issues, "缺少 spec.md")
|
||||||
|
if spec is not None:
|
||||||
|
rel = spec.get("path") or "spec.md"
|
||||||
|
if rel != "spec.md":
|
||||||
|
_issue(issues, f"spec.path 必须是 spec.md,实际: {rel}")
|
||||||
|
if kind == "long":
|
||||||
|
spec_dir = ship_dir / "spec"
|
||||||
|
if not spec_dir.is_dir():
|
||||||
|
_issue(issues, "kind=long 缺少 spec/ 目录")
|
||||||
|
revision = spec.get("revision")
|
||||||
|
prd = spec.get("prd")
|
||||||
|
if revision and not prd:
|
||||||
|
_issue(issues, "long spec.revision 已设但 spec.prd 为空")
|
||||||
|
if prd:
|
||||||
|
prd_file = ship_dir / str(prd)
|
||||||
|
if not prd_file.is_file():
|
||||||
|
_issue(issues, f"缺少 PRD 文件: {prd}")
|
||||||
|
handoff = spec.get("handoff")
|
||||||
|
spec_status = spec.get("specStatus")
|
||||||
|
if spec_status == "approved" and not handoff:
|
||||||
|
_issue(issues, "specStatus=approved 但缺少 spec.handoff")
|
||||||
|
if handoff:
|
||||||
|
handoff_file = ship_dir / str(handoff)
|
||||||
|
if not handoff_file.is_file():
|
||||||
|
_issue(issues, f"缺少 handoff 文件: {handoff}")
|
||||||
|
elif kind == "short":
|
||||||
|
for field in (
|
||||||
|
"revision",
|
||||||
|
"specStatus",
|
||||||
|
"prd",
|
||||||
|
"approvedRevision",
|
||||||
|
"reviews",
|
||||||
|
"handoff",
|
||||||
|
):
|
||||||
|
if spec.get(field) not in (None, "", []):
|
||||||
|
_issue(issues, f"kind=short 时 spec.{field} 必须为空")
|
||||||
|
|
||||||
|
tasks_meta = _mapping(data.get("tasks"), "tasks", issues)
|
||||||
|
tasks_rel = "tasks.yaml"
|
||||||
|
if tasks_meta is not None:
|
||||||
|
tasks_rel = str(tasks_meta.get("path") or "tasks.yaml")
|
||||||
|
tasks_path = ship_dir / tasks_rel
|
||||||
|
if not tasks_path.is_file():
|
||||||
|
_issue(issues, f"缺少 {tasks_rel}")
|
||||||
|
else:
|
||||||
|
issues.extend(_validate_tasks(tasks_path, ship_id, kind))
|
||||||
|
|
||||||
|
delivery = data.get("delivery")
|
||||||
|
if delivery is not None:
|
||||||
|
deliv = _mapping(delivery, "delivery", issues)
|
||||||
|
if deliv is not None:
|
||||||
|
state = deliv.get("state")
|
||||||
|
if state not in DELIVERY_STATE:
|
||||||
|
_issue(issues, f"delivery.state 无效: {state!r}")
|
||||||
|
|
||||||
|
evidence = _mapping(data.get("evidence"), "evidence", issues)
|
||||||
|
if evidence is not None:
|
||||||
|
issues.extend(_validate_evidence(evidence, _project_root(ship_dir)))
|
||||||
|
|
||||||
|
status_md = ship_dir / "status.md"
|
||||||
|
if not status_md.is_file():
|
||||||
|
_issue(issues, "缺少 status.md")
|
||||||
|
|
||||||
|
return issues
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_tasks(path: Path, ship_id: Any, kind: Any) -> list[str]:
|
||||||
|
issues: list[str] = []
|
||||||
|
try:
|
||||||
|
data = load_yaml(path)
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
return [f"无法解析 tasks.yaml: {exc}"]
|
||||||
|
mapping = _mapping(data, "tasks.yaml", issues)
|
||||||
|
if mapping is None:
|
||||||
|
return issues
|
||||||
|
if mapping.get("shipId") != ship_id:
|
||||||
|
_issue(
|
||||||
|
issues,
|
||||||
|
f"tasks.yaml.shipId 与 current.yaml 不一致: "
|
||||||
|
f"{mapping.get('shipId')!r} != {ship_id!r}",
|
||||||
|
)
|
||||||
|
tasks = mapping.get("tasks")
|
||||||
|
if not isinstance(tasks, list) or not tasks:
|
||||||
|
_issue(issues, "tasks.yaml.tasks 必须是非空列表")
|
||||||
|
return issues
|
||||||
|
if kind == "short" and len(tasks) != 1:
|
||||||
|
_issue(issues, f"kind=short 必须恰好 1 条任务,实际 {len(tasks)}")
|
||||||
|
ids: list[str] = []
|
||||||
|
for i, item in enumerate(tasks):
|
||||||
|
prefix = f"tasks[{i}]"
|
||||||
|
row = _mapping(item, prefix, issues)
|
||||||
|
if row is None:
|
||||||
|
continue
|
||||||
|
tid = row.get("id")
|
||||||
|
if not isinstance(tid, str) or not tid.strip():
|
||||||
|
_issue(issues, f"{prefix}.id 不能为空")
|
||||||
|
else:
|
||||||
|
if tid in ids:
|
||||||
|
_issue(issues, f"重复任务 id: {tid}")
|
||||||
|
ids.append(tid)
|
||||||
|
if not isinstance(row.get("title"), str) or not str(row.get("title")).strip():
|
||||||
|
_issue(issues, f"{prefix}.title 不能为空")
|
||||||
|
depends = row.get("dependsOn") or []
|
||||||
|
if not isinstance(depends, list):
|
||||||
|
_issue(issues, f"{prefix}.dependsOn 必须是列表")
|
||||||
|
status = row.get("status")
|
||||||
|
if status not in TASK_STATUS:
|
||||||
|
_issue(issues, f"{prefix}.status 无效: {status!r}")
|
||||||
|
expected = row.get("expected")
|
||||||
|
if not isinstance(expected, list) or not expected:
|
||||||
|
_issue(issues, f"{prefix}.expected 必须是非空列表")
|
||||||
|
verification = row.get("verification")
|
||||||
|
if not isinstance(verification, list) or not verification:
|
||||||
|
_issue(issues, f"{prefix}.verification 必须是非空列表")
|
||||||
|
for i, item in enumerate(tasks):
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
continue
|
||||||
|
for dep in item.get("dependsOn") or []:
|
||||||
|
if dep not in ids:
|
||||||
|
_issue(issues, f"tasks[{i}].dependsOn 引用未知 id: {dep}")
|
||||||
|
if dep == item.get("id"):
|
||||||
|
_issue(issues, f"tasks[{i}] 不能依赖自己")
|
||||||
|
return issues
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_evidence(evidence: dict[str, Any], project_root: Path) -> list[str]:
|
||||||
|
issues: list[str] = []
|
||||||
|
for key in ("testRuns", "artifacts", "deploys", "prs", "commits"):
|
||||||
|
value = evidence.get(key)
|
||||||
|
if value is None:
|
||||||
|
_issue(issues, f"evidence.{key} 缺失")
|
||||||
|
continue
|
||||||
|
if not isinstance(value, list):
|
||||||
|
_issue(issues, f"evidence.{key} 必须是列表")
|
||||||
|
continue
|
||||||
|
for i, item in enumerate(value):
|
||||||
|
prefix = f"evidence.{key}[{i}]"
|
||||||
|
row = _mapping(item, prefix, issues)
|
||||||
|
if row is None:
|
||||||
|
continue
|
||||||
|
if key == "testRuns":
|
||||||
|
rel = row.get("path")
|
||||||
|
if not isinstance(rel, str) or not rel.strip():
|
||||||
|
_issue(issues, f"{prefix}.path 不能为空")
|
||||||
|
else:
|
||||||
|
target = project_root / rel
|
||||||
|
if not target.is_file():
|
||||||
|
_issue(issues, f"{prefix}.path 不存在: {rel}")
|
||||||
|
if row.get("result") not in {
|
||||||
|
"pass",
|
||||||
|
"fail",
|
||||||
|
"environment-fail",
|
||||||
|
"skipped",
|
||||||
|
"not-run",
|
||||||
|
"partial",
|
||||||
|
}:
|
||||||
|
_issue(issues, f"{prefix}.result 无效: {row.get('result')!r}")
|
||||||
|
elif key == "artifacts":
|
||||||
|
if not row.get("sha"):
|
||||||
|
_issue(issues, f"{prefix}.sha 不能为空")
|
||||||
|
elif key == "deploys":
|
||||||
|
if not row.get("env"):
|
||||||
|
_issue(issues, f"{prefix}.env 不能为空")
|
||||||
|
elif key == "prs":
|
||||||
|
if not row.get("url"):
|
||||||
|
_issue(issues, f"{prefix}.url 不能为空")
|
||||||
|
elif key == "commits":
|
||||||
|
if not row.get("sha"):
|
||||||
|
_issue(issues, f"{prefix}.sha 不能为空")
|
||||||
|
if row.get("role") not in COMMIT_ROLE:
|
||||||
|
_issue(issues, f"{prefix}.role 无效: {row.get('role')!r}")
|
||||||
|
return issues
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description="校验 ship 记录目录")
|
||||||
|
parser.add_argument("path", help=".pouch/ship/<id> 或其中的 current.yaml")
|
||||||
|
args = parser.parse_args()
|
||||||
|
path = Path(args.path)
|
||||||
|
ship_dir = path.parent if path.name == "current.yaml" else path
|
||||||
|
issues = validate_ship_dir(ship_dir)
|
||||||
|
if issues:
|
||||||
|
for item in issues:
|
||||||
|
print(f"✗ {item}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
print(f"✓ ship 记录通过: {ship_dir}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
shipId: <ship-id>
|
||||||
|
kind: short
|
||||||
|
title: <short title>
|
||||||
|
status: draft
|
||||||
|
stopAt: null
|
||||||
|
source:
|
||||||
|
kind: user
|
||||||
|
ref: null
|
||||||
|
parentShipId: null
|
||||||
|
childShipIds: []
|
||||||
|
spec:
|
||||||
|
path: spec.md
|
||||||
|
revision: null
|
||||||
|
specStatus: null
|
||||||
|
prd: null
|
||||||
|
approvedRevision: null
|
||||||
|
reviews: null
|
||||||
|
handoff: null
|
||||||
|
tasks:
|
||||||
|
path: tasks.yaml
|
||||||
|
delivery:
|
||||||
|
stopAt: null
|
||||||
|
state: change_ready
|
||||||
|
evidence:
|
||||||
|
testRuns: []
|
||||||
|
artifacts: []
|
||||||
|
deploys: []
|
||||||
|
prs: []
|
||||||
|
commits: []
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# <title>
|
||||||
|
|
||||||
|
## 要做什么
|
||||||
|
|
||||||
|
<用户可见的目标。>
|
||||||
|
|
||||||
|
## 不做什么
|
||||||
|
|
||||||
|
<明确排除的范围。>
|
||||||
|
|
||||||
|
## 可观察验收
|
||||||
|
|
||||||
|
- <能被浏览器、API 或真机看到的结果>
|
||||||
|
|
||||||
|
## 怎么验
|
||||||
|
|
||||||
|
- <命令、页面路径或手工步骤>
|
||||||
|
|
||||||
|
## 故障证据
|
||||||
|
|
||||||
|
<没有故障时删除本节。有则写复现步骤和根因。>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# <title>
|
||||||
|
|
||||||
|
- ship id: `<ship-id>`
|
||||||
|
- kind: `<kind>`
|
||||||
|
- status: `<status>`
|
||||||
|
- stopAt: `<stopAt>`
|
||||||
|
- delivery: `<delivery.state>`
|
||||||
|
|
||||||
|
## spec
|
||||||
|
|
||||||
|
见 `spec.md`。
|
||||||
|
|
||||||
|
## tasks
|
||||||
|
|
||||||
|
见 `tasks.yaml`。
|
||||||
|
|
||||||
|
## evidence
|
||||||
|
|
||||||
|
测试、产物、部署、PR 和 commit 指针写在 `current.yaml` 的 `evidence`。
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
shipId: <ship-id>
|
||||||
|
tasks:
|
||||||
|
- id: T-01
|
||||||
|
title: <short title>
|
||||||
|
dependsOn: []
|
||||||
|
expected:
|
||||||
|
- <observable acceptance signal>
|
||||||
|
verification:
|
||||||
|
- <how to verify>
|
||||||
|
status: open
|
||||||
|
stopAt: null
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# short-ship
|
||||||
|
|
||||||
|
把一次范围清楚的改动(小功能、文案、已确认的 bug)从说明推进到配置允许的 PR、合并或部署。
|
||||||
|
|
||||||
|
## 什么时候使用
|
||||||
|
|
||||||
|
```text
|
||||||
|
给列表页加一个导出按钮,测完后发 PR。
|
||||||
|
```
|
||||||
|
|
||||||
|
或:
|
||||||
|
|
||||||
|
```text
|
||||||
|
修复这个线上回归,完整验证并部署到测试环境。
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用前准备
|
||||||
|
|
||||||
|
- 同时安装 `ship`(记录格式)和 `delivery-loop`。
|
||||||
|
- 目标已经说清,或有可复现的问题描述、截图、日志。
|
||||||
|
- 项目已配置测试环境和所需 builder/tester/deployer 能力。
|
||||||
|
- 若要外部发布,明确允许的停止点或使用项目 profile。
|
||||||
|
|
||||||
|
## Agent 会做什么
|
||||||
|
|
||||||
|
建立 ship 记录,写短需求说明,改代码并验证,再继续完成配置允许的交付阶段。一次交付一个 ship id。
|
||||||
|
|
||||||
|
## 如何判断完成
|
||||||
|
|
||||||
|
你会得到 ship id、说明、测试证据、PR/合并/tag、产物和部署状态;不会只收到“本地测试通过”。
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
---
|
||||||
|
name: short-ship
|
||||||
|
description: >-
|
||||||
|
处理范围已清楚的单次变更:小功能、文案、加按钮,或已有证据的 bug。建立
|
||||||
|
ship 记录、改代码、验证,再按配置发 PR、合并、构建或部署。触发词:short-ship、
|
||||||
|
小需求、小改动、修 bug、改文案、加按钮。复杂需求用 long-ship;已有 PR 用 pr-ship。
|
||||||
|
---
|
||||||
|
|
||||||
|
# Short Ship:单次变更到交付
|
||||||
|
|
||||||
|
把一次范围清楚的改动推进到项目配置允许的停止点。
|
||||||
|
|
||||||
|
## 何时使用
|
||||||
|
|
||||||
|
- 加按钮、改文案、改短描述、单页小功能,目标与验收已经明确。
|
||||||
|
- 用户报告错误、回归、崩溃或明确要求修 bug,并且能指出问题。
|
||||||
|
- 用户要求改完后继续测试、发 PR、合并、构建、发布或部署。
|
||||||
|
|
||||||
|
## 不适用
|
||||||
|
|
||||||
|
- 跨模块、要方案取舍、要多任务协作:使用 `long-ship` 和 `specifier`。
|
||||||
|
- 已有 PR 的合并和发布:使用 `pr-ship`。
|
||||||
|
- 只做根因分析、不改代码:使用 `hunt`。
|
||||||
|
|
||||||
|
## 工作规则
|
||||||
|
|
||||||
|
1. 读取已安装的 `ship` skill。没有 `ship-id` 就从需求生成候选 slug,用
|
||||||
|
`scripts/create_ship.py --kind short` 建 `.pouch/ship/<id>/`;已有目录则校验后复用。
|
||||||
|
写 `spec.md` 四段:要做什么、不做什么、可观察验收、怎么验。有故障证据时加复现和根因。
|
||||||
|
任务保持恰好 1 条。功能任务不要写入 `.pouch/ack/tasks.yaml`。
|
||||||
|
2. 有故障证据时使用 `hunt` 确认根因、复现和验收信号。纯小需求跳过 hunt。
|
||||||
|
3. 修改代码并补或更新回归用例;行为、API、图片、UI 改动不得只跑编译。
|
||||||
|
4. 使用 `tester` 做黑盒、浏览器或真机验证;涉及资源加载时检查真实 URL、状态、类型、
|
||||||
|
内容和截图。跑完立刻用 ship `append_evidence.py --kind testRun` 追加指针。
|
||||||
|
5. 读取并执行 `delivery-loop`。停止点由用户或项目 delivery profile 决定,canonical
|
||||||
|
为 `verified`、`validation_ready`、`review_ready` 或 `released`。每个完成阶段都要把
|
||||||
|
对应指针写入该 ship 的 `current.yaml`;缺指针则阶段未完成。需要产物时只调用
|
||||||
|
builder `obtain.sh --sha <merged-or-pushed-commit>`。
|
||||||
|
6. 范围涨到需要 PRD 或多条任务时,同一 `ship-id` 把 `kind` 改为 `long`,交给
|
||||||
|
`long-ship`,不要另开 id。
|
||||||
|
|
||||||
|
## 安全门
|
||||||
|
|
||||||
|
- 没有 ship-id、spec.md 四段不齐、验收不可观察时停止。
|
||||||
|
- 根因不清、测试缺失、真机不可达、CI/回归失败或工作树有无关改动时停止并说明。
|
||||||
|
- 不强推、不覆盖他人分支、不绕过 PR 审核,不把测试环境成功当生产成功。
|
||||||
|
- 数据迁移、破坏性 API、权限/密钥、安全修复和不可逆生产操作需要额外确认。
|
||||||
|
|
||||||
|
## 必读资料
|
||||||
|
|
||||||
|
- 记录格式:已安装的 `ship` skill 与 `references/record.md`。
|
||||||
|
- 交付阶段:已安装的 `delivery-loop` 及其状态机 reference。
|
||||||
|
- 测试、构建、部署、提交前审查:项目的 `tester`、`builder`、`deployer`、`check`、
|
||||||
|
`releaser`。
|
||||||
|
|
||||||
|
## 完成报告
|
||||||
|
|
||||||
|
报告 ship id、spec 路径、根因(若有)、变更、测试/真机证据指针、停止点、commit、
|
||||||
|
PR/tag、产物摘要、部署状态和未完成项。
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
interface:
|
||||||
|
display_name: "Short Ship"
|
||||||
|
short_description: "Ship a small change or bug fix through verification and delivery"
|
||||||
|
default_prompt: "Use $short-ship to implement this small change, verify it, and continue to the configured delivery stop."
|
||||||
|
policy:
|
||||||
|
allow_implicit_invocation: true
|
||||||
@@ -1,17 +1,18 @@
|
|||||||
# specifier
|
# specifier
|
||||||
|
|
||||||
把复杂需求整理成经过多轮人工评审的 HTML PRD,并在批准后生成交给 ACK 执行的需求 handoff。
|
把复杂需求整理成经过多轮人工评审的 HTML PRD,并在批准后生成交给 long-ship 执行的需求 handoff。
|
||||||
|
|
||||||
## 什么时候使用
|
## 什么时候使用
|
||||||
|
|
||||||
- 要做跨模块、跨页面、跨服务或带数据迁移的复杂功能。
|
- 要做跨模块、跨页面、跨服务或带数据迁移的复杂功能。
|
||||||
- 要先写 PRD、比较方案、梳理风险,再进入开发。
|
- 要先写 PRD、比较方案、梳理风险,再进入开发。
|
||||||
- 需要把 PRD 给人反复评审,确认后再交给 `/ack`。
|
- 需要把 PRD 给人反复评审,确认后再交给 `long-ship`。
|
||||||
|
|
||||||
小改动直接使用 `/ack`;已知 Bug 使用 `/hunt`;单次架构取舍使用 `/think`。
|
小改动直接使用 `short-ship`;已知 Bug 使用 `/hunt`;单次架构取舍使用 `/think`。
|
||||||
|
|
||||||
## 使用前准备
|
## 使用前准备
|
||||||
|
|
||||||
|
- 同时安装 `ship`;批准后的开发走 `long-ship`。
|
||||||
- 项目应位于 Git 仓库中,便于读取现有代码、文档和历史决策。
|
- 项目应位于 Git 仓库中,便于读取现有代码、文档和历史决策。
|
||||||
- 用户需要参与 PRD 评审,并明确批准具体版本。
|
- 用户需要参与 PRD 评审,并明确批准具体版本。
|
||||||
- 不需要外部账号或网络资源;生成的 HTML 应能离线打开。
|
- 不需要外部账号或网络资源;生成的 HTML 应能离线打开。
|
||||||
@@ -20,7 +21,7 @@
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
用 specifier 处理“给后台增加多租户权限管理”这个复杂需求:先调研现状,生成 HTML PRD,
|
用 specifier 处理“给后台增加多租户权限管理”这个复杂需求:先调研现状,生成 HTML PRD,
|
||||||
我会逐轮评审。批准后再生成 ack handoff,不要提前派开发。
|
我会逐轮评审。批准后再生成 long-ship handoff,不要提前派开发。
|
||||||
```
|
```
|
||||||
|
|
||||||
## Agent 会做什么
|
## Agent 会做什么
|
||||||
@@ -29,14 +30,14 @@
|
|||||||
2. 形成目标、范围、非目标、方案、交互/架构、风险、依赖和可观测验收。
|
2. 形成目标、范围、非目标、方案、交互/架构、风险、依赖和可观测验收。
|
||||||
3. 生成可离线打开的版本化 HTML PRD;保留每个历史版本并记录本轮变更。
|
3. 生成可离线打开的版本化 HTML PRD;保留每个历史版本并记录本轮变更。
|
||||||
4. 根据用户意见反复修改,直到用户明确批准指定版本。
|
4. 根据用户意见反复修改,直到用户明确批准指定版本。
|
||||||
5. 仅在批准后生成 `handoff.yaml`,供 `/ack` 创建和执行任务。
|
5. 仅在批准后生成 `spec/handoff.yaml`,供 `long-ship` 创建和执行任务。
|
||||||
6. 用户需要远程查看,或说“把 HTML 渲染出来”时,可调用独立的 `previewer` skill;它用 Node
|
6. 用户需要远程查看,或说“把 HTML 渲染出来”时,可调用独立的 `previewer` skill;它用 Node
|
||||||
渲染并通过 Tailscale 暴露为 HTTP URL,同时返回应该使用的 Tailscale IP。
|
渲染并通过 Tailscale 暴露为 HTTP URL,同时返回应该使用的 Tailscale IP。
|
||||||
|
|
||||||
Specifier 只负责需求规格,不修改业务代码,也不替代 ACK 的开发和独立测试。
|
Specifier 只负责需求规格,不修改业务代码,也不替代 long-ship 的开发和独立测试。
|
||||||
|
|
||||||
## 如何判断完成
|
## 如何判断完成
|
||||||
|
|
||||||
- 评审阶段:得到一个带版本号的 HTML PRD、变更记录、风险和未决问题。
|
- 评审阶段:得到一个带版本号的 HTML PRD、变更记录、风险和未决问题。
|
||||||
- 交接阶段:用户明确批准某个版本,且 `handoff.yaml` 绑定了该版本和完整验收信息。
|
- 交接阶段:用户明确批准某个版本,且 `spec/handoff.yaml` 绑定了该版本和完整验收信息。
|
||||||
- 开发阶段:改用 `/ack`,由 ACK 负责 Developer、Test、回归和交付闭环。
|
- 开发阶段:改用 `long-ship`,任务和证据写在同一个 ship id 下。
|
||||||
|
|||||||
+48
-38
@@ -2,14 +2,15 @@
|
|||||||
name: specifier
|
name: specifier
|
||||||
description: >-
|
description: >-
|
||||||
把复杂或跨模块需求整理成可评审、可版本化的 HTML PRD,并在用户明确批准某个版本后
|
把复杂或跨模块需求整理成可评审、可版本化的 HTML PRD,并在用户明确批准某个版本后
|
||||||
生成交给 ack 的结构化 handoff。用户说做复杂功能、写 PRD、梳理需求、设计方案、需求
|
生成交给 long-ship 的结构化 handoff。用户说做复杂功能、写 PRD、梳理需求、设计方案、
|
||||||
评审、反复改需求或交给 ack 开发时使用;小改动、Bug 根因排查和单纯架构讨论不使用。
|
需求评审、反复改需求或交给 long-ship 开发时使用;小改动、Bug 根因排查和单纯架构讨论
|
||||||
|
不使用。
|
||||||
---
|
---
|
||||||
|
|
||||||
# Specifier:复杂需求规格化
|
# Specifier:复杂需求规格化
|
||||||
|
|
||||||
把模糊需求变成经过人确认的需求基线,再交给 `ack` 执行。HTML 是给人评审的正式交付物,
|
把模糊需求变成经过人确认的需求基线,再交给 `long-ship` 执行。HTML 是给人评审的正式交付物,
|
||||||
结构化状态和 handoff 是给 Agent/ACK 消费的机器接口。
|
结构化状态和 handoff 是给 ship 记录消费的机器接口。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -17,46 +18,53 @@ description: >-
|
|||||||
|
|
||||||
- 用户要做跨模块、跨页面、跨服务或有数据迁移的复杂功能。
|
- 用户要做跨模块、跨页面、跨服务或有数据迁移的复杂功能。
|
||||||
- 用户要求写 PRD、产品方案、需求规格、架构/交互方案,并需要多轮确认。
|
- 用户要求写 PRD、产品方案、需求规格、架构/交互方案,并需要多轮确认。
|
||||||
- 用户要求把确认后的需求交给 `/ack` 开发。
|
- 用户要求把确认后的需求交给 `long-ship` 开发。
|
||||||
|
|
||||||
## 不适用
|
## 不适用
|
||||||
|
|
||||||
- 单文件或范围明确的小改动:直接交给 `/ack`。
|
- 单文件或范围明确的小改动:使用 `short-ship`。
|
||||||
- 已有复现证据的 Bug:使用 `/hunt` 找根因,再按需要交给 `/ack`。
|
- 已有复现证据的 Bug:使用 `/hunt` 找根因,再按需要交给 `short-ship`。
|
||||||
- 只问一个技术选型或一次性方案判断:使用 `/think`。
|
- 只问一个技术选型或一次性方案判断:使用 `/think`。
|
||||||
- 用户尚未批准的需求:不得创建 ACK 任务、派 worker、改业务代码或开始交付。
|
- 用户尚未批准的需求:不得创建开发任务、派 worker、改业务代码或开始交付。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 工作目录和事实源
|
## 工作目录和事实源
|
||||||
|
|
||||||
每个需求使用独立目录 `.pouch/specifier/features/<feature-id>/`。开始前检查目录是否已存在,
|
每个需求使用独立 ship 目录 `.pouch/ship/<ship-id>/`。规格文件写在 `spec/`。开始前读取
|
||||||
不要覆盖既有版本;已有需求要进入对应版本的 `review` 或 `revise` 模式。
|
已安装的 `ship` skill;已有需求要进入对应版本的 `review` 或 `revise` 模式。
|
||||||
|
|
||||||
```text
|
```text
|
||||||
.pouch/specifier/features/<feature-id>/
|
.pouch/ship/<ship-id>/
|
||||||
├── current.yaml # 当前版本、状态和批准信息
|
├── current.yaml # ship 总账;spec.revision / specStatus 在这里
|
||||||
├── brief.md # 需求输入和现状摘要
|
├── spec.md # 短摘要
|
||||||
├── prd-v001.html # 人类评审版本,不覆盖旧版本
|
├── tasks.yaml # 批准后由 long-ship 写入,本 skill 不写
|
||||||
├── review.yaml # 每轮意见、修改请求和确认
|
└── spec/
|
||||||
├── decisions.yaml # 关键方案决策
|
├── brief.md
|
||||||
├── risks.yaml # 风险、依赖和缓解措施
|
├── prd-v001.html
|
||||||
└── handoff.yaml # 仅 approved 后生成,交给 ack
|
├── review.yaml
|
||||||
|
├── decisions.yaml
|
||||||
|
├── risks.yaml
|
||||||
|
└── handoff.yaml # 仅 approved 后生成,交给 long-ship
|
||||||
```
|
```
|
||||||
|
|
||||||
目录名使用稳定、短、小写连字符的 feature ID。若用户未提供,先从需求生成候选并说明;
|
目录名使用稳定、短、小写连字符的 ship id。若用户未提供,先从需求生成候选并说明;
|
||||||
不要用日期、随机数或当前分支名冒充业务 ID。
|
不要用日期、随机数或当前分支名冒充业务 ID。
|
||||||
|
|
||||||
`.pouch/specifier/` 只保存需求规格状态,不复制 skill 内容,不修改项目的 `AGENTS.md`、
|
若只有旧路径 `.pouch/specifier/features/<id>/`,按 ship `record.md` 迁到
|
||||||
`CLAUDE.md` 或已有 ACK 状态。HTML 可引用仓库中的文档和代码路径,但不能依赖外部网络资源。
|
`.pouch/ship/<id>/spec/` 后再继续。不要两处同时写。
|
||||||
|
|
||||||
|
`.pouch/ship/<id>/spec/` 只保存需求规格,不复制 skill 内容,不修改项目的 `AGENTS.md`、
|
||||||
|
`CLAUDE.md` 或 ACK 缺陷板。HTML 可引用仓库中的文档和代码路径,但不能依赖外部网络资源。
|
||||||
|
|
||||||
## 模式路由
|
## 模式路由
|
||||||
|
|
||||||
- **新建**:探查现状,建立 brief,生成 `prd-v001.html`,状态为 `review`。
|
- **新建**:探查现状,建立 `spec/brief.md`,生成 `spec/prd-v001.html`,把
|
||||||
- **评审 / 修改**:读取当前版本和 `review.yaml`,把用户意见转成修改项,生成新版本,
|
`spec.specStatus` 设为 `review`。
|
||||||
|
- **评审 / 修改**:读取当前版本和 `spec/review.yaml`,把用户意见转成修改项,生成新版本,
|
||||||
展示变更摘要和仍未决的问题。
|
展示变更摘要和仍未决的问题。
|
||||||
- **批准**:只在用户明确批准指定版本后,把状态改为 `approved`,冻结需求基线。
|
- **批准**:只在用户明确批准指定版本后,把 `spec.specStatus` 改为 `approved`,冻结需求基线。
|
||||||
- **交接**:仅针对 `approved` 版本生成 `handoff.yaml`;用户要求继续开发时把它交给 `/ack`。
|
- **交接**:仅针对 `approved` 版本生成 `spec/handoff.yaml`;用户要求继续开发时把它交给 `long-ship`。
|
||||||
- **状态**:只读报告当前版本、待决策、风险和是否可以交接。
|
- **状态**:只读报告当前版本、待决策、风险和是否可以交接。
|
||||||
|
|
||||||
详细评审状态机和确认规则见 [review-loop.md](references/review-loop.md)。HTML 页面结构和
|
详细评审状态机和确认规则见 [review-loop.md](references/review-loop.md)。HTML 页面结构和
|
||||||
@@ -64,14 +72,14 @@ description: >-
|
|||||||
|
|
||||||
## 新建和调研
|
## 新建和调研
|
||||||
|
|
||||||
1. 先判断需求是否真的复杂;不复杂时给出转 `/ack` 的建议,不强行生成 PRD。
|
1. 先判断需求是否真的复杂;不复杂时给出转 `short-ship` 的建议,不强行生成 PRD。
|
||||||
2. 读取项目覆盖层、相关文档、历史决策和代码;优先确认现有能力、模块边界、数据模型、
|
2. 读取项目覆盖层、相关文档、历史决策和代码;优先确认现有能力、模块边界、数据模型、
|
||||||
API、权限、运行约束和回归用例。不要从 README 或猜测中虚构项目事实。
|
API、权限、运行约束和回归用例。不要从 README 或猜测中虚构项目事实。
|
||||||
3. 形成现状与目标差异:背景、用户、场景、目标、非目标、范围、约束、成功标准。
|
3. 形成现状与目标差异:背景、用户、场景、目标、非目标、范围、约束、成功标准。
|
||||||
4. 对关键方案给出取舍,记录推荐方案、被否决方案、最脆弱假设、风险、依赖和回滚方向。
|
4. 对关键方案给出取舍,记录推荐方案、被否决方案、最脆弱假设、风险、依赖和回滚方向。
|
||||||
5. 描述必要的产品流程、交互状态、领域模型、状态流、API/数据变化和非功能要求。
|
5. 描述必要的产品流程、交互状态、领域模型、状态流、API/数据变化和非功能要求。
|
||||||
6. 按可独立验证的垂直切片拆任务,写出依赖、关键路径、`expected` 和 `verification`,
|
6. 按可独立验证的垂直切片拆任务,写出依赖、关键路径、`expected` 和 `verification`,
|
||||||
但不要写入 `.pouch/ack/tasks.yaml`。
|
但不要写入 `.pouch/ack/tasks.yaml` 或本 ship 的 `tasks.yaml`(任务由 long-ship 在批准后创建)。
|
||||||
7. 按模板生成自包含 HTML PRD,并在回复中说明文件路径、版本、待确认问题和下一步。
|
7. 按模板生成自包含 HTML PRD,并在回复中说明文件路径、版本、待确认问题和下一步。
|
||||||
8. 用户需要在其它设备查看 HTML,或说“把这个 HTML 渲染出来”时,调用已安装的
|
8. 用户需要在其它设备查看 HTML,或说“把这个 HTML 渲染出来”时,调用已安装的
|
||||||
`previewer` 能力传入该文件;不要在 `specifier` 内复制 Node/Tailscale Serve 逻辑。
|
`previewer` 能力传入该文件;不要在 `specifier` 内复制 Node/Tailscale Serve 逻辑。
|
||||||
@@ -98,20 +106,21 @@ description: >-
|
|||||||
需求评审状态,HTML 被展示不等于用户批准。回复中要同时给出 Tailscale IP 和 URL,不能只
|
需求评审状态,HTML 被展示不等于用户批准。回复中要同时给出 Tailscale IP 和 URL,不能只
|
||||||
说“已经渲染”或只返回本机 `127.0.0.1`。
|
说“已经渲染”或只返回本机 `127.0.0.1`。
|
||||||
|
|
||||||
## 批准和 ACK 交接
|
## 批准和 long-ship 交接
|
||||||
|
|
||||||
批准前只允许写 specifier 自己的需求目录。批准后仍不自动启动 worker;先生成 `handoff.yaml`,
|
批准前只允许写该 ship 的 spec 目录和 `current.yaml` 的 spec 字段。批准后仍不自动启动
|
||||||
其中必须绑定:feature ID、PRD 版本和路径、需求范围、非目标、决策、任务依赖、验收信号、
|
worker;先生成 `spec/handoff.yaml`,其中必须绑定:ship id、PRD 版本和路径、需求范围、
|
||||||
风险、回滚/发布注意事项和回归候选。
|
非目标、决策、任务依赖、验收信号、风险、回滚/发布注意事项和回归候选。同步
|
||||||
|
`current.yaml` 的 `spec.specStatus`、`spec.approvedRevision`、`spec.handoff`。
|
||||||
|
|
||||||
`handoff.yaml` 是 ACK 的输入,不是 ACK 的执行记录。用户明确要求继续开发时,提示使用:
|
`handoff.yaml` 是 `long-ship` 的输入,不是执行记录。用户明确要求继续开发时,提示使用:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
/ack 使用 specifier handoff:.pouch/specifier/features/<feature-id>/handoff.yaml
|
/long-ship 使用 handoff:.pouch/ship/<ship-id>/spec/handoff.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
ACK 应引用批准的 HTML 版本,不复制另一份可漂移的 PRD。若 ACK 不存在、handoff 缺字段或
|
long-ship 应引用批准的 HTML 版本,不复制另一份可漂移的 PRD。handoff 缺字段或需求状态
|
||||||
需求状态不是 `approved`,停止交接并报告缺口。
|
不是 `approved` 时,停止交接并报告缺口。
|
||||||
|
|
||||||
## 安全边界
|
## 安全边界
|
||||||
|
|
||||||
@@ -123,6 +132,7 @@ ACK 应引用批准的 HTML 版本,不复制另一份可漂移的 PRD。若 AC
|
|||||||
|
|
||||||
## 验证
|
## 验证
|
||||||
|
|
||||||
完成前检查:HTML 可离线打开、版本号与 `current.yaml` 一致、旧版本未被覆盖、每个未决问题
|
完成前检查:HTML 可离线打开、版本号与 ship `current.yaml` 的 `spec.revision` 一致、旧版本
|
||||||
有明确状态、每个任务有可观测验收、handoff 只在 `approved` 后存在且绑定正确的 PRD 版本。
|
未被覆盖、每个未决问题有明确状态、每个任务有可观测验收、handoff 只在 `approved` 后存在
|
||||||
需要交给 ACK 时,先确认 handoff schema 与用户批准版本一致,再停止等待 ACK。
|
且绑定正确的 PRD 版本。需要交给 long-ship 时,先确认 handoff schema 与用户批准版本一致,
|
||||||
|
再停止等待 long-ship。
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
interface:
|
interface:
|
||||||
display_name: "Specifier"
|
display_name: "Specifier"
|
||||||
short_description: "Draft and review complex requirements as HTML PRDs"
|
short_description: "Draft and review complex requirements as HTML PRDs"
|
||||||
default_prompt: "Use $specifier to turn this complex feature request into a reviewable HTML PRD."
|
default_prompt: "Use $specifier to turn this complex feature request into a reviewable HTML PRD in the ship record."
|
||||||
policy:
|
policy:
|
||||||
allow_implicit_invocation: true
|
allow_implicit_invocation: true
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ HTML 是给人离线评审的交付物,不是运行中的产品页面。它必
|
|||||||
- 颜色和状态不能只依靠颜色表达;为状态提供文字标签。
|
- 颜色和状态不能只依靠颜色表达;为状态提供文字标签。
|
||||||
- 长表格允许横向滚动,代码或 API 示例使用 `pre`,正文保持可复制。
|
- 长表格允许横向滚动,代码或 API 示例使用 `pre`,正文保持可复制。
|
||||||
- `h1` 只有一个;标题层级连续;链接和表格有可读的焦点/边框状态。
|
- `h1` 只有一个;标题层级连续;链接和表格有可读的焦点/边框状态。
|
||||||
- 页首显示 feature ID、PRD 版本、状态、生成时间和基于的版本。
|
- 页首显示 ship id、PRD 版本、状态、生成时间和基于的版本。
|
||||||
- 变更内容用文字摘要或 `ins` / `del` 标记表达,不要只用颜色标记。
|
- 变更内容用文字摘要或 `ins` / `del` 标记表达,不要只用颜色标记。
|
||||||
|
|
||||||
## 内容质量
|
## 内容质量
|
||||||
@@ -36,4 +36,4 @@ HTML 是给人离线评审的交付物,不是运行中的产品页面。它必
|
|||||||
- 把“体验好”“性能足够”“支持权限”等不可验证表述改成可观察信号或明确待决策项。
|
- 把“体验好”“性能足够”“支持权限”等不可验证表述改成可观察信号或明确待决策项。
|
||||||
- 不把实现方案伪装成用户需求;需求、决策和任务分开写。
|
- 不把实现方案伪装成用户需求;需求、决策和任务分开写。
|
||||||
- 未知事实写“待确认”,并加入未决问题,不填看似合理的数字、接口或页面。
|
- 未知事实写“待确认”,并加入未决问题,不填看似合理的数字、接口或页面。
|
||||||
- 每条验收都能被 ACK 的 Test 按步骤观察;测试工具不可用时仍能区分产品失败和环境失败。
|
- 每条验收都能被 long-ship / Test 按步骤观察;测试工具不可用时仍能区分产品失败和环境失败。
|
||||||
|
|||||||
@@ -9,15 +9,17 @@
|
|||||||
|
|
||||||
## 状态
|
## 状态
|
||||||
|
|
||||||
`current.yaml` 至少记录:
|
ship `current.yaml` 的 spec 字段至少记录:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
featureId: tenant-permissions
|
shipId: tenant-permissions
|
||||||
currentRevision: v002
|
kind: long
|
||||||
status: review
|
spec:
|
||||||
prd: prd-v002.html
|
revision: v002
|
||||||
approvedRevision: null
|
specStatus: review
|
||||||
reviews: review.yaml
|
prd: spec/prd-v002.html
|
||||||
|
approvedRevision: null
|
||||||
|
reviews: spec/review.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
允许的状态:
|
允许的状态:
|
||||||
@@ -55,11 +57,11 @@ reviews: review.yaml
|
|||||||
|
|
||||||
## 每轮评审
|
## 每轮评审
|
||||||
|
|
||||||
1. 读取 `current.yaml`、当前 HTML 和所有未解决的 review 项;不要只读用户最新一句话。
|
1. 读取 ship `current.yaml`、当前 HTML 和所有未解决的 review 项;不要只读用户最新一句话。
|
||||||
2. 将新反馈归类:`change`、`decision`、`question`、`rejected`、`parked`。
|
2. 将新反馈归类:`change`、`decision`、`question`、`rejected`、`parked`。
|
||||||
3. 说明每个反馈对范围、架构、任务和验收的影响。
|
3. 说明每个反馈对范围、架构、任务和验收的影响。
|
||||||
4. 生成新版本 HTML,保留旧文件。
|
4. 生成新版本 HTML,保留旧文件。
|
||||||
5. 更新 `current.yaml` 和 `review.yaml`,并在回复中给出:新版本路径、变更摘要、未决问题、
|
5. 更新 ship `current.yaml` 的 spec 字段和 `spec/review.yaml`,并在回复中给出:新版本路径、变更摘要、未决问题、
|
||||||
是否仍需批准。
|
是否仍需批准。
|
||||||
|
|
||||||
如果反馈只影响措辞,也生成新版本;评审记录需要保持可追溯。若用户明确说不采纳,记录理由,
|
如果反馈只影响措辞,也生成新版本;评审记录需要保持可追溯。若用户明确说不采纳,记录理由,
|
||||||
@@ -82,7 +84,8 @@ reviews: review.yaml
|
|||||||
- HTML 版本、状态文件和 review 记录一致。
|
- HTML 版本、状态文件和 review 记录一致。
|
||||||
|
|
||||||
如果用户说“继续”“可以”“看起来不错”,但没有绑定版本,继续保持 `review`,请用户确认
|
如果用户说“继续”“可以”“看起来不错”,但没有绑定版本,继续保持 `review`,请用户确认
|
||||||
具体版本。批准后把 `approvedRevision` 写入 `current.yaml`,再生成 handoff;不要同时启动 ACK。
|
具体版本。批准后把 `spec.approvedRevision` 写入 ship `current.yaml`,再生成
|
||||||
|
`spec/handoff.yaml`;不要同时启动 long-ship 或 ACK。
|
||||||
|
|
||||||
## 批准后的变更
|
## 批准后的变更
|
||||||
|
|
||||||
@@ -94,5 +97,5 @@ reviews: review.yaml
|
|||||||
4. 标记受影响的任务、验收、风险和 ACK handoff 字段;
|
4. 标记受影响的任务、验收、风险和 ACK handoff 字段;
|
||||||
5. 重新等待用户批准。
|
5. 重新等待用户批准。
|
||||||
|
|
||||||
已经进入 ACK 的任务不会因 HTML 更新而自动改变。需要执行变更时,明确告诉用户应创建新的
|
已经进入 long-ship 的任务不会因 HTML 更新而自动改变。需要执行变更时,明确告诉用户应
|
||||||
ACK 任务或由 ACK 重新规划,不能静默改写执行记录。
|
修订本 ship 的 `tasks.yaml` 或重新规划,不能静默改写执行记录。
|
||||||
|
|||||||
@@ -1,6 +1,16 @@
|
|||||||
featureId: <feature-id>
|
# 已并入 ship current.yaml 的 spec 字段。新建需求请用 ship 模板,不要单独写这份文件。
|
||||||
currentRevision: v001
|
# 迁移旧 .pouch/specifier/features/<id>/current.yaml 时按此对照:
|
||||||
status: review
|
# featureId -> shipId
|
||||||
prd: prd-v001.html
|
# currentRevision -> spec.revision
|
||||||
approvedRevision: null
|
# status -> spec.specStatus
|
||||||
reviews: review.yaml
|
# prd -> spec.prd(路径加上 spec/ 前缀)
|
||||||
|
# approvedRevision -> spec.approvedRevision
|
||||||
|
# reviews -> spec.reviews
|
||||||
|
shipId: <ship-id>
|
||||||
|
spec:
|
||||||
|
revision: v001
|
||||||
|
specStatus: review
|
||||||
|
prd: spec/prd-v001.html
|
||||||
|
approvedRevision: null
|
||||||
|
reviews: spec/review.yaml
|
||||||
|
handoff: null
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
featureId: <feature-id>
|
shipId: <ship-id>
|
||||||
specifierRevision: v001
|
specifierRevision: v001
|
||||||
status: approved
|
status: approved
|
||||||
prd:
|
prd:
|
||||||
path: prd-v001.html
|
path: spec/prd-v001.html
|
||||||
revision: v001
|
revision: v001
|
||||||
scope:
|
scope:
|
||||||
goals: []
|
goals: []
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
<main>
|
<main>
|
||||||
<article>
|
<article>
|
||||||
<header>
|
<header>
|
||||||
<p class="meta"><span class="badge">Feature: {{feature_id}}</span><span class="badge">PRD: {{revision}}</span><span class="badge status">状态:{{status}}</span></p>
|
<p class="meta"><span class="badge">Ship: {{ship_id}}</span><span class="badge">PRD: {{revision}}</span><span class="badge status">状态:{{status}}</span></p>
|
||||||
<h1>{{feature_title}}</h1>
|
<h1>{{feature_title}}</h1>
|
||||||
<p>{{one_sentence_summary}}</p>
|
<p>{{one_sentence_summary}}</p>
|
||||||
<p class="muted">生成时间:{{generated_at}} · 基于版本:{{changed_from}}</p>
|
<p class="muted">生成时间:{{generated_at}} · 基于版本:{{changed_from}}</p>
|
||||||
|
|||||||
@@ -32,8 +32,14 @@ class CatalogTests(unittest.TestCase):
|
|||||||
"releaser",
|
"releaser",
|
||||||
"note-style",
|
"note-style",
|
||||||
"pouch",
|
"pouch",
|
||||||
|
"short-ship",
|
||||||
|
"long-ship",
|
||||||
|
"ship",
|
||||||
|
"pr-ship",
|
||||||
# 旧名兼容占位,仍须可列出(见 test_manage_release_is_deprecation_stub)
|
# 旧名兼容占位,仍须可列出(见 test_manage_release_is_deprecation_stub)
|
||||||
"manage-release",
|
"manage-release",
|
||||||
|
"bugfix-ship",
|
||||||
|
"requirement-flow",
|
||||||
):
|
):
|
||||||
self.assertIn(f"| {name} |", result.stdout)
|
self.assertIn(f"| {name} |", result.stdout)
|
||||||
self.assertIn("/ack 初始化", result.stdout)
|
self.assertIn("/ack 初始化", result.stdout)
|
||||||
@@ -51,6 +57,28 @@ class CatalogTests(unittest.TestCase):
|
|||||||
self.assertFalse((stub / "references").exists())
|
self.assertFalse((stub / "references").exists())
|
||||||
self.assertFalse((stub / "templates").exists())
|
self.assertFalse((stub / "templates").exists())
|
||||||
|
|
||||||
|
def test_bugfix_ship_is_deprecation_stub(self) -> None:
|
||||||
|
stub = REPO_ROOT / "skills" / "bugfix-ship"
|
||||||
|
skill = (stub / "SKILL.md").read_text(encoding="utf-8")
|
||||||
|
self.assertIn("name: bugfix-ship", skill)
|
||||||
|
self.assertIn("已弃用", skill)
|
||||||
|
self.assertIn("../short-ship/SKILL.md", skill)
|
||||||
|
self.assertTrue((REPO_ROOT / "skills" / "short-ship" / "SKILL.md").is_file())
|
||||||
|
self.assertFalse((stub / "references").exists())
|
||||||
|
self.assertFalse((stub / "templates").exists())
|
||||||
|
self.assertFalse((stub / "agents").exists())
|
||||||
|
|
||||||
|
def test_requirement_flow_is_deprecation_stub(self) -> None:
|
||||||
|
stub = REPO_ROOT / "skills" / "requirement-flow"
|
||||||
|
skill = (stub / "SKILL.md").read_text(encoding="utf-8")
|
||||||
|
self.assertIn("name: requirement-flow", skill)
|
||||||
|
self.assertIn("已弃用", skill)
|
||||||
|
self.assertIn("../long-ship/SKILL.md", skill)
|
||||||
|
self.assertTrue((REPO_ROOT / "skills" / "long-ship" / "SKILL.md").is_file())
|
||||||
|
self.assertFalse((stub / "references").exists())
|
||||||
|
self.assertFalse((stub / "templates").exists())
|
||||||
|
self.assertFalse((stub / "agents").exists())
|
||||||
|
|
||||||
def test_missing_readme_fails(self) -> None:
|
def test_missing_readme_fails(self) -> None:
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
skill = Path(tmp) / "demo"
|
skill = Path(tmp) / "demo"
|
||||||
|
|||||||
@@ -0,0 +1,159 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
CREATE = REPO_ROOT / "skills" / "ship" / "scripts" / "create_ship.py"
|
||||||
|
VALIDATE = REPO_ROOT / "skills" / "ship" / "scripts" / "validate_ship.py"
|
||||||
|
APPEND = REPO_ROOT / "skills" / "ship" / "scripts" / "append_evidence.py"
|
||||||
|
|
||||||
|
|
||||||
|
def _run(script: Path, *args: str, cwd: Path | None = None) -> subprocess.CompletedProcess[str]:
|
||||||
|
return subprocess.run(
|
||||||
|
[sys.executable, str(script), *args],
|
||||||
|
check=False,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
cwd=cwd,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class YamlEmptyCollectionsTests(unittest.TestCase):
|
||||||
|
def test_roundtrip_empty_list_and_dict(self) -> None:
|
||||||
|
from pouch.yaml_io import safe_dump, safe_load
|
||||||
|
|
||||||
|
data = {"items": [], "meta": {}}
|
||||||
|
loaded = safe_load(safe_dump(data))
|
||||||
|
self.assertEqual(loaded, data)
|
||||||
|
|
||||||
|
|
||||||
|
class CreateAndValidateTests(unittest.TestCase):
|
||||||
|
def test_create_short_ship_validates(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
root = Path(tmp)
|
||||||
|
result = _run(
|
||||||
|
CREATE,
|
||||||
|
"--project-root",
|
||||||
|
str(root),
|
||||||
|
"--ship-id",
|
||||||
|
"add-export-button",
|
||||||
|
"--kind",
|
||||||
|
"short",
|
||||||
|
"--title",
|
||||||
|
"列表页增加导出按钮",
|
||||||
|
)
|
||||||
|
self.assertEqual(result.returncode, 0, result.stderr)
|
||||||
|
ship_dir = root / ".pouch" / "ship" / "add-export-button"
|
||||||
|
self.assertTrue((ship_dir / "current.yaml").is_file())
|
||||||
|
self.assertTrue((ship_dir / "spec.md").is_file())
|
||||||
|
self.assertFalse((ship_dir / "spec").is_dir())
|
||||||
|
check = _run(VALIDATE, str(ship_dir))
|
||||||
|
self.assertEqual(check.returncode, 0, check.stderr)
|
||||||
|
|
||||||
|
def test_create_long_ship_has_spec_dir(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
root = Path(tmp)
|
||||||
|
result = _run(
|
||||||
|
CREATE,
|
||||||
|
"--project-root",
|
||||||
|
str(root),
|
||||||
|
"--ship-id",
|
||||||
|
"tenant-permissions",
|
||||||
|
"--kind",
|
||||||
|
"long",
|
||||||
|
"--title",
|
||||||
|
"多租户权限",
|
||||||
|
)
|
||||||
|
self.assertEqual(result.returncode, 0, result.stderr)
|
||||||
|
ship_dir = root / ".pouch" / "ship" / "tenant-permissions"
|
||||||
|
self.assertTrue((ship_dir / "spec").is_dir())
|
||||||
|
check = _run(VALIDATE, str(ship_dir))
|
||||||
|
self.assertEqual(check.returncode, 0, check.stderr)
|
||||||
|
|
||||||
|
def test_rejects_date_id(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
result = _run(
|
||||||
|
CREATE,
|
||||||
|
"--project-root",
|
||||||
|
tmp,
|
||||||
|
"--ship-id",
|
||||||
|
"20261013",
|
||||||
|
"--kind",
|
||||||
|
"short",
|
||||||
|
"--title",
|
||||||
|
"nope",
|
||||||
|
)
|
||||||
|
self.assertNotEqual(result.returncode, 0)
|
||||||
|
self.assertIn("日期", result.stderr)
|
||||||
|
|
||||||
|
def test_append_test_run_pointer(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
root = Path(tmp)
|
||||||
|
created = _run(
|
||||||
|
CREATE,
|
||||||
|
"--project-root",
|
||||||
|
str(root),
|
||||||
|
"--ship-id",
|
||||||
|
"add-export-button",
|
||||||
|
"--kind",
|
||||||
|
"short",
|
||||||
|
"--title",
|
||||||
|
"列表页增加导出按钮",
|
||||||
|
)
|
||||||
|
self.assertEqual(created.returncode, 0, created.stderr)
|
||||||
|
run_dir = root / ".pouch" / "tester" / "runs"
|
||||||
|
run_dir.mkdir(parents=True)
|
||||||
|
run_path = run_dir / "20261013T010000Z-smoke-ab12.yaml"
|
||||||
|
run_path.write_text("runId: demo\n", encoding="utf-8")
|
||||||
|
ship_dir = root / ".pouch" / "ship" / "add-export-button"
|
||||||
|
appended = _run(
|
||||||
|
APPEND,
|
||||||
|
str(ship_dir),
|
||||||
|
"--kind",
|
||||||
|
"testRun",
|
||||||
|
"--path",
|
||||||
|
".pouch/tester/runs/20261013T010000Z-smoke-ab12.yaml",
|
||||||
|
"--result",
|
||||||
|
"pass",
|
||||||
|
"--suite",
|
||||||
|
"smoke",
|
||||||
|
)
|
||||||
|
self.assertEqual(appended.returncode, 0, appended.stderr)
|
||||||
|
text = (ship_dir / "current.yaml").read_text(encoding="utf-8")
|
||||||
|
self.assertIn("20261013T010000Z-smoke-ab12.yaml", text)
|
||||||
|
|
||||||
|
def test_missing_pointer_file_fails(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
root = Path(tmp)
|
||||||
|
_run(
|
||||||
|
CREATE,
|
||||||
|
"--project-root",
|
||||||
|
str(root),
|
||||||
|
"--ship-id",
|
||||||
|
"add-export-button",
|
||||||
|
"--kind",
|
||||||
|
"short",
|
||||||
|
"--title",
|
||||||
|
"列表页增加导出按钮",
|
||||||
|
)
|
||||||
|
ship_dir = root / ".pouch" / "ship" / "add-export-button"
|
||||||
|
appended = _run(
|
||||||
|
APPEND,
|
||||||
|
str(ship_dir),
|
||||||
|
"--kind",
|
||||||
|
"testRun",
|
||||||
|
"--path",
|
||||||
|
".pouch/tester/runs/missing.yaml",
|
||||||
|
"--result",
|
||||||
|
"pass",
|
||||||
|
)
|
||||||
|
self.assertNotEqual(appended.returncode, 0)
|
||||||
|
self.assertIn("不存在", appended.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user