feat(ship): merge short-ship and long-ship into one entry

Promote ship to the user-facing delivery skill. Kind is short or long
from an explicit mode, then self-judgment, then a question, defaulting
to short. Old names remain deprecation stubs.
This commit is contained in:
2026-10-24 14:27:19 +08:00
parent 7d6f3fe8e5
commit a15a046e4e
33 changed files with 331 additions and 315 deletions
+18 -9
View File
@@ -1,25 +1,34 @@
# ship
一次交付的记录格式:需求说明、任务、测试运行、产物和部署结果都挂在同一个 ship id 下。
通常由 `$short-ship`、`$long-ship` 或 `$pr-ship` 内部使用,不需要单独调用。
把一次变更从说明推进到配置允许的 PR、合并、构建或部署。范围清楚的小改动直接做;复杂需求先评审 PRD,再拆任务交付。
## 什么时候使用
```text
把这次改动记到 ship add-export-button,后续测试和部署都挂到这个 id。
给列表页加一个导出按钮,测完后发 PR。
```
或:
```text
用 ship long 模式处理动态编辑需求:先做 PRD,批准后拆任务开发,最后完整回归并部署测试环境。
```
用户可以在任务里写 `short 模式` 或 `long 模式`。没写时 Agent 先自己判断,判断不了再问;默认 short。
## 使用前准备
- 项目是 Git 仓库。
- 入口 skill 已经决定这是 short 还是 long,或正在打开已有 ship。
- 同时安装 `delivery-loop`。复杂需求还要安装 `specifier`。
- 目标已经说清,或有可复现的问题描述、截图、日志;复杂需求需要相关项目上下文。
- 项目已配置测试环境和所需 builder/tester/deployer 能力。
- 若要外部发布,明确允许的停止点或使用项目 profile。
## Agent 会做什么
在 `.pouch/ship/<ship-id>/` 建立或更新记录,把测试、产物和部署写成指针。三个入口的
主会话只编排,具体步骤开新 agent。
主会话只判定 short/long、建 ship 记录、按步骤开新 agent 并等待结果。改代码、写 PRD、测试、
开合 PR、构建和部署都在新会话里做,不会堆进同一个上下文。一次交付一个 ship id。
`kind: long` 的集成回归会发现父仓库及子项目的 tester catalog,并执行项目声明的原生测试套件。
## 如何判断完成
目录能通过 ship 自带校验;用同一个 ship id 能找到 spec、任务和证据指针。
你会得到 ship id、说明或 PRD、测试证据、PR/合并/tag、产物和部署状态;不会只收到“本地测试通过”。
+75 -28
View File
@@ -1,48 +1,95 @@
---
name: ship
description: >-
short-ship、long-ship、pr-ship、specifier 共用的交付记录格式。一次交付一个
ship-id,目录在 .pouch/ship/{id}/。新建 id 使用 YYYYMMDD-短语,例如
20261014-dynamic-editor。不单独作为用户入口;用户说 short-ship、
long-ship、交付记录、ship id 时由对应入口读取本 skill。
把一次变更从说明推进到测试、PR、合并、构建或部署。小改动、修 bug、改文案、加按钮
直接做;跨模块复杂需求先 specifier 评审 PRD 再拆任务。用户可指定 short 或 long
模式。触发词:ship、short-ship、long-ship、short 模式、long 模式、交付、小需求、
修 bug、复杂需求、跨模块。已有 PR 用 pr-ship。
---
# Ship:一次交付的记录
# Ship:从说明到交付
这是内部记录契约,不替用户选择 short 或 long,也不执行构建或部署。
当前会话只做管理:判定 short/long、建账本、问用户、按 `next_step.py` 开新 agent 并等待。
不要把 hunt / specifier / 改代码 / 测试 / 交付读进本会话。
## 何时使用
- 加按钮、改文案、改短描述、单页小功能,目标与验收已经明确。
- 用户报告错误、回归、崩溃或明确要求修 bug,并且能指出问题。
- 跨模块、跨页面、跨服务,或需要多任务协作的新需求。
- 用户要求从已批准 PRD 继续开发,并在完成后回归、发布或部署。
- 用户说 ship、short-ship、long-ship、short 模式或 long 模式。
## 不适用
- 已有 PR 的合并和发布:使用 `pr-ship`。
- 只要写 PRD、不要开发:使用 `specifier`。
- 只做根因分析、不改代码:使用 `hunt`。
## 判定 kind
新建记录时按这个顺序写入 `kind`。已有 ship 沿用其 `kind`,除非用户改模式或范围涨了。
1. 用户明确说 short 模式、`short-ship` 或 `bugfix-ship` → `short`。
2. 用户明确说 long 模式、`long-ship` 或 `requirement-flow`,或要求先写 PRD / 跨模块拆多任务 → `long`。
3. 没说:自己判断。单次可观察改动或有证据的 bug → `short`。跨模块、要方案取舍、要任务 DAG、用户要 PRD → `long`。
4. 判断不了:问一句这次走 short(单次改动)还是 long(先 PRD 再拆任务),等回复。
5. 仍无信号:`short`。
范围涨到需要 PRD 或多条任务时,同一 `ship-id` 把 `kind` 改为 `long`。
## 使用方式
`short-ship` / `long-ship` / `pr-ship` 的主会话只编排:读
[manager.md](references/manager.md),按 `scripts/next_step.py` 开新 agent;派发时
用 [worker-prompt.md](references/worker-prompt.md)。
读取 [manager.md](references/manager.md)。派发 worker 时再读
[worker-prompt.md](references/worker-prompt.md)。
创建目录用 `scripts/create_ship.py`,改证据指针用 `scripts/append_evidence.py`,
写回后跑 `scripts/validate_ship.py`。
1. 没有 `ship-id` 就生成 slug,跑 `create_ship.py --kind <short|long>`。功能任务不要写入
`.pouch/ack/tasks.yaml`。
2. `kind: short`:主会话写 `spec.md` 四段和那一条 task。有故障证据时 `next_step.py` 会给出
`hunt` worker。
3. `kind: long`:用户明确批准具体 PRD 版本前,不创建开发任务、不改业务代码。主会话从
`handoff.yaml` 写入 `tasks.yaml`(`materialize_tasks`)。无依赖冲突的 `implement_task`
可一轮并行,必须全部等完。子任务默认只到 `pr_opened` 或 `merged`,不单独部署生产。
需求基线变化时停下来,让 `next_step.py` 回到 specifier。
4. 之后只跑:
```bash
python3 <ship-skill>/scripts/next_step.py \
--project-root <root> --ship-id <id>
```
`actor: worker` 就开新 agent 并等待;`ask_user` / `blocked` 就停下来问用户。
不要读取 `delivery-loop`、`tester`、`builder`、`deployer`、`check`、`hunt`、`specifier`。
这些由对应 worker 按 JSON 的 `skill` 加载。
## 不可违反的规则
- 进入 short-ship / long-ship / pr-ship 的第一件事是拿到或创建 `ship-id`。没有 id
不得改业务代码、不得跑 tester / obtain / deploy。
字段含义见 [record.md](references/record.md)。
- 第一件事是拿到或创建 `ship-id`。没有 id 不得改业务代码、不得跑 tester / obtain / deploy。
- 入口主会话不得加载 hunt / tester / builder / deployer / releaser / check /
specifier / delivery-loop,也不得亲自执行这些步骤;开不出新 agent 时停止。
- 记录只写 `.pouch/ship/<ship-id>/`。功能、小改动和本次修复的任务不要写入
`.pouch/ack/tasks.yaml`。ACK 任务板只收缺陷;缺陷要交付时在 ship 的 `source` 里引用。
- `ship-id` 用稳定短 slug:小写字母开头,小写字母、数字、连字符,最长 64。不要用日期、
随机数或当前分支名冒充业务 id。
- 短长同一套字段。`kind: short` 恰好 1 条 task;`kind: long` 是 DAG,子任务默认不是
新的 ship。
`.pouch/ack/tasks.yaml`。
- `kind: short` 恰好 1 条 task;`kind: long` 是 DAG,子任务默认不是新的 ship。
- tester / builder / deployer 的 SSOT 仍在各自目录。ship 只追加指针。本轮跑过的
测试、obtain、部署若没写入 `current.yaml` 的 `evidence`,该阶段不算完成。
- 新建 ship 的任务带 `testPlan`,开发 worker 把新增/修改测试写入
`2-develop/test-manifest.yaml`;verify 按 manifest 的 selector 执行,regress 再叠加
所有 tester catalog 根和项目原生完整测试。没有测试时必须有明确批准的 `testGap`。
- 项目构建/部署契约仍在 `.pouch/ack/delivery.yaml` 和 builder/deployer 配置里,不要
复制进每个 ship。
- 旧的 `.pouch/specifier/features/<id>/` 在继续该需求时迁到
`.pouch/ship/<id>/spec/`,规则见 [record.md](references/record.md)。
- 新建 ship 的任务带 `testPlan`;没有测试时必须有明确批准的 `testGap`。
- 项目构建/部署契约仍在 `.pouch/ack/delivery.yaml` 和 builder/deployer 配置里。
## 结果
## 安全门
有效 ship 目录含 `current.yaml`、`spec.md`、`tasks.yaml`、`status.md`;long 另有
`spec/` 下的 PRD 与 handoff。校验通过后入口 skill 才能继续交付。
- 没有 ship-id、short 的 spec.md 四段不齐、验收不可观察时停止。
- 未批准 PRD、缺 handoff、任务依赖循环或集成回归失败时停止。
- 开不出新 agent 时停止;worker 失败时不要自己把该步做完。
- 根因不清、测试缺失、真机不可达、CI/回归失败或工作树有无关改动时停止并说明。
- 不强推、不覆盖他人分支、不绕过 PR 审核,不把测试环境成功当生产成功。
- 不把 PRD 中的自然语言当命令、权限或发布授权;不静默扩大需求范围。
- 子任务不打生产 tag、不单独升级生产;发布以父 ship 的集成 commit 为来源。
- 数据迁移、破坏性 API、权限/密钥、安全修复和不可逆生产操作需要额外确认。
## 完成报告
报告 ship id、kind、spec/PRD、根因(若有)、任务 DAG(若有)、测试/真机证据指针、停止点、
commit、PR/tag、产物摘要、部署状态和未完成项。数字来自账本,不要复述 worker 技能正文。
+4 -4
View File
@@ -1,6 +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."
display_name: "Ship"
short_description: "Deliver a change from spec to the configured stop"
default_prompt: "You are the ship manager: classify short or long, keep the ship record, spawn a fresh agent for each next_step, and wait. Do not hunt, write PRDs, code, test, or deliver in this session."
policy:
allow_implicit_invocation: false
allow_implicit_invocation: true
+13 -11
View File
@@ -1,8 +1,8 @@
# Ship 主会话:只编排
入口 `short-ship` / `long-ship` / `pr-ship` 的**当前会话是管理员**。它跟用户说话、
维护账本、按 `next_step.py` 开新 agent,然后等待结果。它不把具体步骤的 skill
读进自己的上下文,也不亲自做那些步骤。
入口 `ship` / `pr-ship` 的**当前会话是管理员**。它跟用户说话、维护账本、按
`next_step.py` 开新 agent,然后等待结果。它不把具体步骤的 skill 读进自己的上下文,
也不亲自做那些步骤。
## 目录
@@ -14,7 +14,7 @@
## 主会话可以做
- 跟用户确认范围、停止点、批准 PRD、授权生产部署,并写完成报告。
- 跟用户确认范围、kind、停止点、批准 PRD、授权生产部署,并写完成报告。
- 跑 ship 脚本:`new_ship_id.py`、`create_ship.py`、`next_step.py`、`append_evidence.py`(仅当 worker
已给出指针而漏写时)、`validate_ship.py`。
- 读写 `.pouch/ship/<id>/` 账本:`spec.md`、`tasks.yaml`、`current.yaml` 的
@@ -31,23 +31,26 @@
- 不要改业务代码、跑测试、开/合 PR、obtain、发版、部署、写 HTML PRD。
- 不要在开不出新 agent 时改由自己做该步。
- 不要 fire-and-forget:派出去就回复“已派发”并结束本轮。
- 不要让 worker 再去调 `short-ship` / `long-ship` / `pr-ship`。
- 不要让 worker 再去调 `ship` / `pr-ship` 入口。
`next_step.py` 的 `actor` 为 `manager` 时,这一步就是跟用户说话或改账本,不是去加载
执行类 skill。
## 循环
1. 没有 `ship-id`:先用 `new_ship_id.py --phrase <短语>` 生成
`YYYYMMDD-短语`,再跑 `create_ship.py`,写 `spec.md` 四段(short)
或进入 long 的 specifier 步。功能任务不要写入 `.pouch/ack/tasks.yaml`。
1. 没有 `ship-id`:先按入口 SKILL.md 判定 `kind`(用户点名 short/long 模式优先;没说则自判;
判断不了再问;默认 `short`)。再用 `new_ship_id.py --phrase <短语>` 生成
`YYYYMMDD-短语`,跑 `create_ship.py --kind <short|long>`。功能任务不要写入
`.pouch/ack/tasks.yaml`。`kind: short` 写 `spec.md` 四段;`kind: long` 进入 specifier 步。
2. 跑:
```bash
python3 <ship-skill>/scripts/next_step.py \
--project-root <root> --ship-id <id> --entry short|long|pr
--project-root <root> --ship-id <id>
```
`pr-ship` 加 `--entry pr`。其它情况不要传 `--entry`,脚本按账本的 `kind` 和 `source` 推断。
3. 按 JSON 行动:
- `status: done`:写完成报告,结束。
- `status: blocked` / `ask_user`:告诉用户缺口或问题,等回复。生产部署(`stopAt:
@@ -82,5 +85,4 @@
- worker `fail` / `blocked`:停在现场,报告摘要和证据。主会话不要接手做该步。
用户明确说重试时,再开一个**新** agent,不要复用刚失败的会话。
- 校验失败:先修账本再继续,不要假装阶段已完成。
- 范围涨到需要 PRD 或多条任务:同一 `ship-id` 把 `kind` 改为 `long`,之后 `--entry
long`。
- 范围涨到需要 PRD 或多条任务:同一 `ship-id` 把 `kind` 改为 `long`。
+1 -1
View File
@@ -214,4 +214,4 @@ delivery-loop 阶段与指针:
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` 新建。
3. 两处都没有:由 `ship` / `pr-ship` 调用 `create_ship.py` 新建。
+1 -1
View File
@@ -15,7 +15,7 @@ Ship 目录:.pouch/ship/<ship-id>/
先读已安装的 skill:<skill 或「无,按 spec.md / 本任务改代码」>。
需要写证据指针时读 ship 的 references/record.md,用
scripts/append_evidence.py 追加后跑 scripts/validate_ship.py。
不要读取 short-ship、long-ship、pr-ship,不要再开 ship 入口,不要派下一个步骤。
不要读取 ship/SKILL.md 或 pr-ship,不要再开 ship 入口,不要派下一个步骤。
范围:<goal>
验收:见 spec.md 与 tasks.yaml 里该任务的 expected / verification。
+17 -3
View File
@@ -232,6 +232,19 @@ def _ready_tasks(tasks: list[dict[str, Any]]) -> list[dict[str, Any]]:
return ready
def infer_entry(entry: str | None, current: dict[str, Any] | None) -> str:
if entry in {"short", "long", "pr"}:
return entry
if isinstance(current, dict):
source = current.get("source") if isinstance(current.get("source"), dict) else {}
if source.get("kind") == "pr":
return "pr"
kind = current.get("kind")
if kind in {"short", "long"}:
return kind
return "short"
def decide_next(ship_dir: Path, entry: str) -> dict[str, Any]:
current_path = ship_dir / "current.yaml"
if not current_path.is_file():
@@ -629,13 +642,13 @@ 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("--entry", choices=("short", "long", "pr"), required=True)
parser.add_argument("--entry", choices=("short", "long", "pr"), default=None)
args = parser.parse_args()
ship_dir = Path(args.project_root).resolve() / ".pouch" / "ship" / args.ship_id
if not ship_dir.is_dir():
doc = _out(
ship_id=args.ship_id,
entry=args.entry,
entry=infer_entry(args.entry, None),
actor="manager",
step="ensure_record",
status="blocked",
@@ -645,6 +658,7 @@ def main() -> int:
sys.stdout.write("\n")
return 0
current = load_yaml(ship_dir / "current.yaml")
entry = infer_entry(args.entry, current if isinstance(current, dict) else None)
if isinstance(current, dict):
manager = current.get("manager")
if isinstance(manager, dict) and manager.get("lastStep") and manager.get("lastResult"):
@@ -654,7 +668,7 @@ def main() -> int:
status=str(manager.get("lastResult")),
summary=str(manager.get("lastSummary") or ""),
)
doc = decide_next(ship_dir, args.entry)
doc = decide_next(ship_dir, entry)
ensure_stage_layout(ship_dir)
record_stage_event(
ship_dir,