chore: sync pouch skill workflows
This commit is contained in:
@@ -47,7 +47,7 @@ pouch add builder -g # 全局
|
||||
并负责上层编排。
|
||||
|
||||
1. `makefile.builder` 目标:`help/build/clean/version` + 条件 `deb/docker/push*`,
|
||||
变量 `ARCHES/ARCH/VERSION/DIST_DIR/PROJECT_NAME`。
|
||||
以及 `obtain` / `venue`。变量 `ARCHES/ARCH/VERSION/DIST_DIR/PROJECT_NAME/BUILD_VENUE`。
|
||||
`VERSION` 通过 `include <builder>/scripts/version.mk` 从 Git 推导。
|
||||
多架构清单是 `ARCHES`,见下方「多架构」。
|
||||
2. 运行 `python3 -I -S <builder>/scripts/check.py . --ready` 直到构建项 PASS。
|
||||
@@ -227,5 +227,5 @@ Docker 只有你点名镜像时才走。token 不会写进输出、提交或回
|
||||
| `templates/makefile.android.builder` | Android APK + copy 轨道骨架 |
|
||||
| `templates/makefile.ios.builder` | iOS IPA + copy 轨道骨架 |
|
||||
|
||||
环境变量契约、脚本解析顺序(`$BUILDER_SKILL_DIR` → `~/.pouch/skills/builder/scripts/`)、
|
||||
环境变量契约、脚本解析顺序(`$BUILDER_SKILL_DIR` → `~/.pouch` → `~/.agents`)、
|
||||
脏工作树策略等完整规则见 contract.md。
|
||||
|
||||
@@ -49,7 +49,7 @@ make -f makefile.builder <target>
|
||||
| `ios` | 有 iOS IPA 的项目 | 可与 `build` 相同。产出 `$(DIST_DIR)/<name>_<version>_<arch>.ipa`;只构建不上传 |
|
||||
| `arches` | 可选(新模板必备) | 打印一行空格分隔的 `ARCHES`;无此目标视为存量 makefile,隐式单架构 |
|
||||
| `push-deb` | 同时有多种发布轨道的项目 | 仅调 builder 的 `upload_deb.sh`。未指定 `ARCH=` 时上传 `ARCHES` 中每一项对应的 `.deb`;指定了则只上传那一个 |
|
||||
| `push-docker` | 同时有多种发布轨道的项目 | 仅调 builder 的 `publish_docker.sh` |
|
||||
| `push-docker` | 同时有多种发布轨道的项目 | `BUILD_VENUE=local` 时仅调 `publish_docker.sh`;`ci` 时仅调 `obtain.sh` |
|
||||
| `push-copy` | 同时有多种发布轨道的项目 | 仅调 builder 的 `copy_artifact.sh`;由它按配置选择内置拷贝或自定义脚本 |
|
||||
| `push` | 单一产物类型时必备;多产物项目为聚合 | 依序调用对应 push-* 或直接调脚本;是发布的唯一 make 入口 |
|
||||
| `obtain` | 有产物的项目 | 调用 `obtain.sh`,对精确 commit SHA 拿到已存在的产物。`BUILD_VENUE=ci` 时等待该 SHA 的 forge 流水线再 inspect,不在本机 `docker push` |
|
||||
|
||||
@@ -31,12 +31,15 @@ DIST_DIR ?= dist
|
||||
|
||||
ifneq ($(BUILDER_SKILL_DIR),)
|
||||
include $(BUILDER_SKILL_DIR)/scripts/version.mk
|
||||
else
|
||||
BUILDER_SCRIPTS := $(BUILDER_SKILL_DIR)/scripts
|
||||
else ifneq ($(wildcard $(HOME)/.pouch/skills/builder/scripts/version.mk),)
|
||||
include $(HOME)/.pouch/skills/builder/scripts/version.mk
|
||||
BUILDER_SCRIPTS := $(HOME)/.pouch/skills/builder/scripts
|
||||
else
|
||||
include $(HOME)/.agents/skills/builder/scripts/version.mk
|
||||
BUILDER_SCRIPTS := $(HOME)/.agents/skills/builder/scripts
|
||||
endif
|
||||
|
||||
BUILDER_SCRIPTS := $(or $(BUILDER_SKILL_DIR),$(HOME)/.pouch/skills/builder)/scripts
|
||||
|
||||
.PHONY: help version arches venue clean build deb docker obtain push push-deb push-docker
|
||||
|
||||
help:
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
|
||||
## 什么时候使用
|
||||
|
||||
通常由 `$short-ship`、`$pr-ship` 或 `$long-ship` 内部读取,不需要单独调用。
|
||||
通常由 `$short-ship`、`$pr-ship` 或 `$long-ship` 派发的 worker 读取,不需要单独调用。
|
||||
入口主会话不要把本 skill 读进自己的上下文。
|
||||
|
||||
```text
|
||||
按 delivery-loop 把当前 ship 推进到 review_ready。
|
||||
|
||||
@@ -11,9 +11,10 @@ description: >-
|
||||
|
||||
## 使用方式
|
||||
|
||||
只有在入口 skill 明确要求执行交付时才读取
|
||||
`references/state-machine.md`。入口 skill 负责确定任务类型和目标;本 skill 负责阶段、
|
||||
停止点、证据和安全门。
|
||||
由 `short-ship` / `long-ship` / `pr-ship` **派出来的 worker** 读取本 skill 和
|
||||
`references/state-machine.md`。入口主会话不要读取或执行本 skill。
|
||||
|
||||
入口决定任务类型、停止点和派哪一步;本 skill 负责该步的阶段要求、证据和安全门。
|
||||
|
||||
## 不可违反的规则
|
||||
|
||||
|
||||
@@ -1,6 +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."
|
||||
default_prompt: "You are the long-ship manager: keep the ship record, spawn a fresh agent for each next_step, and wait. Do not write the PRD, code, or run delivery in this session."
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
|
||||
## Agent 会做什么
|
||||
|
||||
先绑到一个 ship id,检查 PR 是否可合并;合并后重新以目标分支 commit 回归和构建,再按配置继续发布或部署。
|
||||
主会话只绑 ship id,再按步骤开新 agent 并等待。检查、合并、回归、构建和部署都在新
|
||||
会话里做;合并后的回归以目标分支 commit 为准。
|
||||
|
||||
## 如何判断完成
|
||||
|
||||
|
||||
+20
-18
@@ -7,7 +7,8 @@ description: >-
|
||||
|
||||
# PR Ship:合并后的完整交付
|
||||
|
||||
重点是合并后的真实状态:PR 分支通过不代表目标分支已回归,也不代表产物和生产已更新。
|
||||
当前会话只做管理:绑 ship id、按 `next_step.py` 开新 agent 并等待。不要在本会话
|
||||
审 diff、合并或跑回归。
|
||||
|
||||
## 何时使用
|
||||
|
||||
@@ -22,29 +23,30 @@ description: >-
|
||||
|
||||
## 工作规则
|
||||
|
||||
1. 读取已安装的 `ship` skill。从 PR 正文、分支名或用户给出的 id 绑定已有 ship;没有则
|
||||
用 PR 标题生成 slug,创建 `kind: short` 的薄记录,`source.kind: pr`。之后证据都写进
|
||||
这个 ship id。
|
||||
2. 读取 PR 的目标/源分支、差异、关联任务、审核状态、CI、冲突和变更风险;使用 `check`。
|
||||
3. 审核、CI 或必需检查缺失时停止。若发现可修复问题,只有当前请求明确包含修复时才在 PR 分支上处理,
|
||||
然后重新验证;不能静默改变 PR 范围。
|
||||
4. 具备合并条件后执行合并,并重新获取目标分支;记录合并后的 commit,后续所有回归和构建都以它为来源。
|
||||
5. 读取并执行 `delivery-loop` 的状态机,运行完整回归;失败时停在合并后的中间状态或回到修复流程,不部署失败版本。
|
||||
每个完成阶段把指针写入该 ship 的 `current.yaml`。
|
||||
6. 只有项目 profile 和当前授权允许时才构建、发布和部署;生产部署完成后做节点状态和关键路径检查。
|
||||
1. 读取已安装 ship 的 [manager.md](../ship/references/manager.md)。派发 worker 时再读
|
||||
[worker-prompt.md](../ship/references/worker-prompt.md)。从 PR 正文、分支名或
|
||||
用户给出的 id 绑定已有 ship;没有则用 PR 标题生成 slug,创建 `kind: short` 的
|
||||
薄记录,`source.kind: pr`。之后证据都写进这个 ship id。
|
||||
2. 只跑:
|
||||
|
||||
```bash
|
||||
python3 <ship-skill>/scripts/next_step.py \
|
||||
--project-root <root> --ship-id <id> --entry pr
|
||||
```
|
||||
|
||||
检查、修复、合并、回归、obtain、发布、部署都是 worker。主会话只写 `source.ref`
|
||||
和等待结果。需要字段含义时再读 [record.md](../ship/references/record.md)。
|
||||
3. 合并后的回归和构建必须以合并 commit 为来源;worker 会写 `commits.role: merged`。
|
||||
|
||||
不要读取 `check`、`delivery-loop`、`tester`、`builder`、`deployer`、`releaser`。
|
||||
|
||||
## 安全门
|
||||
|
||||
- 开不出新 agent 时停止;worker 失败时不要自己把该步做完。
|
||||
- 不绕过审核、CI、分支保护或冲突;不强推、不重写已共享历史。
|
||||
- 不用 PR 源分支的旧 APK/镜像证明合并版本已发布。
|
||||
- 目标分支变化、合并后代码漂移、回归失败、产物来源不明或部署健康检查失败时停止。
|
||||
|
||||
## 必读资料
|
||||
|
||||
- 记录格式:已安装的 `ship` skill 与 `references/record.md`。
|
||||
- 执行交付阶段时读取已安装的 `delivery-loop` skill 及其状态机 reference。
|
||||
- 发布版本时遵循 `releaser`;构建/部署/回归分别遵循 `builder`、`deployer`、`tester`。
|
||||
|
||||
## 完成报告
|
||||
|
||||
报告 ship id、PR/MR、审核/CI 结果、合并 commit、完整回归证据、产物摘要、tag、部署目标、健康检查和回滚点。
|
||||
报告 ship id、PR/MR、审核/CI 结果、合并 commit、完整回归证据、产物摘要、tag、部署目标、健康检查和回滚点。数字来自账本。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
interface:
|
||||
display_name: "PR Ship"
|
||||
short_description: "Merge, regress, build, and deploy a PR"
|
||||
default_prompt: "Use $pr-ship to process this PR through merge, full regression, and the configured deployment stop."
|
||||
default_prompt: "You are the pr-ship manager: bind the ship id, spawn a fresh agent for each next_step, and wait. Do not review, merge, or regress in this session."
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
|
||||
## Agent 会做什么
|
||||
|
||||
在 `.pouch/ship/<ship-id>/` 建立或更新记录,把测试、产物和部署写成指针,而不是另写一套缺陷任务。
|
||||
在 `.pouch/ship/<ship-id>/` 建立或更新记录,把测试、产物和部署写成指针。三个入口的
|
||||
主会话只编排,具体步骤开新 agent。
|
||||
|
||||
## 如何判断完成
|
||||
|
||||
|
||||
@@ -141,6 +141,17 @@ def validate_ship_dir(ship_dir: Path) -> list[str]:
|
||||
if state not in DELIVERY_STATE:
|
||||
_issue(issues, f"delivery.state 无效: {state!r}")
|
||||
|
||||
manager = data.get("manager")
|
||||
if manager is not None:
|
||||
mgr = _mapping(manager, "manager", issues)
|
||||
if mgr is not None:
|
||||
result = mgr.get("lastResult")
|
||||
if result not in (None, "pass", "fail", "blocked", "ask"):
|
||||
_issue(issues, f"manager.lastResult 无效: {result!r}")
|
||||
in_flight = mgr.get("inFlight")
|
||||
if in_flight not in (None, "") and not isinstance(in_flight, str):
|
||||
_issue(issues, "manager.inFlight 必须是字符串或空")
|
||||
|
||||
evidence = _mapping(data.get("evidence"), "evidence", issues)
|
||||
if evidence is not None:
|
||||
issues.extend(_validate_evidence(evidence, _project_root(ship_dir)))
|
||||
|
||||
@@ -23,7 +23,8 @@
|
||||
|
||||
## Agent 会做什么
|
||||
|
||||
建立 ship 记录,写短需求说明,改代码并验证,再继续完成配置允许的交付阶段。一次交付一个 ship id。
|
||||
主会话只建 ship 记录、写短说明、按步骤开新 agent 并等待结果。改代码、测试、开合 PR、
|
||||
构建和部署都在新会话里做,不会堆进同一个上下文。一次交付一个 ship id。
|
||||
|
||||
## 如何判断完成
|
||||
|
||||
|
||||
+23
-23
@@ -8,7 +8,8 @@ description: >-
|
||||
|
||||
# Short Ship:单次变更到交付
|
||||
|
||||
把一次范围清楚的改动推进到项目配置允许的停止点。
|
||||
当前会话只做管理:建账本、问用户、按 `next_step.py` 开新 agent 并等待。不要把
|
||||
hunt / 改代码 / 测试 / 交付读进本会话。
|
||||
|
||||
## 何时使用
|
||||
|
||||
@@ -24,36 +25,35 @@ description: >-
|
||||
|
||||
## 工作规则
|
||||
|
||||
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。
|
||||
1. 读取已安装 ship 的 [manager.md](../ship/references/manager.md)。派发 worker 时再读
|
||||
[worker-prompt.md](../ship/references/worker-prompt.md)。没有 `ship-id` 就生成
|
||||
slug,跑 `create_ship.py --kind short`。功能任务不要写入 `.pouch/ack/tasks.yaml`。
|
||||
2. 主会话只写 `spec.md` 四段和那一条 task。有故障证据时 `next_step.py` 会给出
|
||||
`hunt` worker,不要在本会话读 hunt。
|
||||
3. 之后只跑:
|
||||
|
||||
```bash
|
||||
python3 <ship-skill>/scripts/next_step.py \
|
||||
--project-root <root> --ship-id <id> --entry short
|
||||
```
|
||||
|
||||
`actor: worker` 就开新 agent 并等待;`ask_user` / `blocked` 就停下来问用户。
|
||||
需要字段含义时再读 [record.md](../ship/references/record.md)。
|
||||
4. 范围涨到需要 PRD 或多条任务时,同一 `ship-id` 把 `kind` 改为 `long`,改用
|
||||
`--entry long`。
|
||||
|
||||
不要读取 `delivery-loop`、`tester`、`builder`、`deployer`、`check`、`hunt`。
|
||||
这些由对应 worker 按 JSON 的 `skill` 加载。
|
||||
|
||||
## 安全门
|
||||
|
||||
- 没有 ship-id、spec.md 四段不齐、验收不可观察时停止。
|
||||
- 开不出新 agent 时停止;worker 失败时不要自己把该步做完。
|
||||
- 根因不清、测试缺失、真机不可达、CI/回归失败或工作树有无关改动时停止并说明。
|
||||
- 不强推、不覆盖他人分支、不绕过 PR 审核,不把测试环境成功当生产成功。
|
||||
- 数据迁移、破坏性 API、权限/密钥、安全修复和不可逆生产操作需要额外确认。
|
||||
|
||||
## 必读资料
|
||||
|
||||
- 记录格式:已安装的 `ship` skill 与 `references/record.md`。
|
||||
- 交付阶段:已安装的 `delivery-loop` 及其状态机 reference。
|
||||
- 测试、构建、部署、提交前审查:项目的 `tester`、`builder`、`deployer`、`check`、
|
||||
`releaser`。
|
||||
|
||||
## 完成报告
|
||||
|
||||
报告 ship id、spec 路径、根因(若有)、变更、测试/真机证据指针、停止点、commit、
|
||||
PR/tag、产物摘要、部署状态和未完成项。
|
||||
PR/tag、产物摘要、部署状态和未完成项。数字来自账本,不要复述 worker 技能正文。
|
||||
|
||||
@@ -1,6 +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."
|
||||
default_prompt: "You are the short-ship manager: keep the ship record, spawn a fresh agent for each next_step, and wait. Do not hunt, code, test, or deliver in this session."
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
|
||||
@@ -66,6 +66,8 @@ description: >-
|
||||
- **批准**:只在用户明确批准指定版本后,把 `spec.specStatus` 改为 `approved`,冻结需求基线。
|
||||
- **交接**:仅针对 `approved` 版本生成 `spec/handoff.yaml`;用户要求继续开发时把它交给 `long-ship`。
|
||||
- **状态**:只读报告当前版本、待决策、风险和是否可以交接。
|
||||
- 作为 `long-ship` 派来的 worker:只做 prompt 里的那一个模式,写完 `spec/` 就返回;
|
||||
不要再调用 `long-ship`,不要改业务代码。
|
||||
|
||||
详细评审状态机和确认规则见 [review-loop.md](references/review-loop.md)。HTML 页面结构和
|
||||
渲染要求见 [html-prd.md](references/html-prd.md)。只有需要对应模式时才读取它们。
|
||||
|
||||
@@ -1,15 +1,44 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from pouch.yaml_io import safe_dump, safe_load
|
||||
|
||||
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"
|
||||
NEXT = REPO_ROOT / "skills" / "ship" / "scripts" / "next_step.py"
|
||||
|
||||
FILLED_SPEC = """# 导出按钮
|
||||
|
||||
## 要做什么
|
||||
|
||||
列表页给登录用户一个导出按钮。
|
||||
|
||||
## 不做什么
|
||||
|
||||
不改权限模型。
|
||||
|
||||
## 可观察验收
|
||||
|
||||
- 登录后列表页可见导出按钮
|
||||
|
||||
## 怎么验
|
||||
|
||||
- 浏览器打开 /list 看到按钮
|
||||
"""
|
||||
|
||||
FAULT_SPEC = FILLED_SPEC + """
|
||||
## 故障证据
|
||||
|
||||
线上列表页没有导出按钮。复现:登录后打开 /list。
|
||||
"""
|
||||
|
||||
|
||||
def _run(script: Path, *args: str, cwd: Path | None = None) -> subprocess.CompletedProcess[str]:
|
||||
@@ -155,5 +184,173 @@ class CreateAndValidateTests(unittest.TestCase):
|
||||
self.assertIn("不存在", appended.stderr)
|
||||
|
||||
|
||||
def _create(root: Path, ship_id: str, kind: str, *, stop_at: str | None = None) -> Path:
|
||||
args = [
|
||||
"--project-root",
|
||||
str(root),
|
||||
"--ship-id",
|
||||
ship_id,
|
||||
"--kind",
|
||||
kind,
|
||||
"--title",
|
||||
"列表页增加导出按钮",
|
||||
]
|
||||
if stop_at:
|
||||
args.extend(["--stop-at", stop_at])
|
||||
result = _run(CREATE, *args)
|
||||
if result.returncode != 0:
|
||||
raise AssertionError(result.stderr)
|
||||
return root / ".pouch" / "ship" / ship_id
|
||||
|
||||
|
||||
def _next(root: Path, ship_id: str, entry: str) -> dict:
|
||||
result = _run(
|
||||
NEXT,
|
||||
"--project-root",
|
||||
str(root),
|
||||
"--ship-id",
|
||||
ship_id,
|
||||
"--entry",
|
||||
entry,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise AssertionError(result.stderr or result.stdout)
|
||||
return json.loads(result.stdout)
|
||||
|
||||
|
||||
def _patch_current(ship_dir: Path, **fields) -> None:
|
||||
path = ship_dir / "current.yaml"
|
||||
data = safe_load(path.read_text(encoding="utf-8"))
|
||||
for key, value in fields.items():
|
||||
if key == "delivery_state":
|
||||
data.setdefault("delivery", {})["state"] = value
|
||||
elif key == "manager":
|
||||
data["manager"] = value
|
||||
elif key == "source":
|
||||
data["source"] = value
|
||||
elif key == "spec":
|
||||
data["spec"].update(value)
|
||||
else:
|
||||
data[key] = value
|
||||
path.write_text(safe_dump(data), encoding="utf-8")
|
||||
|
||||
|
||||
def _fill_task(ship_dir: Path) -> None:
|
||||
path = ship_dir / "tasks.yaml"
|
||||
data = safe_load(path.read_text(encoding="utf-8"))
|
||||
data["tasks"][0]["expected"] = ["登录用户看到导出"]
|
||||
data["tasks"][0]["verification"] = ["打开 /list"]
|
||||
path.write_text(safe_dump(data), encoding="utf-8")
|
||||
|
||||
|
||||
class NextStepTests(unittest.TestCase):
|
||||
def test_new_short_ship_writes_spec(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
_create(root, "add-export-button", "short")
|
||||
doc = _next(root, "add-export-button", "short")
|
||||
self.assertEqual(doc["step"], "write_spec")
|
||||
self.assertEqual(doc["actor"], "manager")
|
||||
|
||||
def test_filled_short_without_stop_asks_user(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
ship_dir = _create(root, "add-export-button", "short")
|
||||
(ship_dir / "spec.md").write_text(FILLED_SPEC, encoding="utf-8")
|
||||
_fill_task(ship_dir)
|
||||
doc = _next(root, "add-export-button", "short")
|
||||
self.assertEqual(doc["status"], "ask_user")
|
||||
|
||||
def test_short_implement_then_verify(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
ship_dir = _create(root, "add-export-button", "short", stop_at="verified")
|
||||
(ship_dir / "spec.md").write_text(FILLED_SPEC, encoding="utf-8")
|
||||
_fill_task(ship_dir)
|
||||
doc = _next(root, "add-export-button", "short")
|
||||
self.assertEqual(doc["step"], "implement")
|
||||
self.assertEqual(doc["actor"], "worker")
|
||||
self.assertIsNone(doc["skill"])
|
||||
|
||||
_patch_current(
|
||||
ship_dir,
|
||||
manager={"lastStep": "implement", "lastResult": "pass", "inFlight": None},
|
||||
)
|
||||
data = safe_load((ship_dir / "current.yaml").read_text(encoding="utf-8"))
|
||||
data["evidence"]["commits"] = [{"sha": "abc1234deadbeef", "role": "head"}]
|
||||
(ship_dir / "current.yaml").write_text(safe_dump(data), encoding="utf-8")
|
||||
doc = _next(root, "add-export-button", "short")
|
||||
self.assertEqual(doc["step"], "verify")
|
||||
self.assertEqual(doc["skill"], "tester")
|
||||
|
||||
_patch_current(ship_dir, delivery_state="verified")
|
||||
doc = _next(root, "add-export-button", "short")
|
||||
self.assertEqual(doc["status"], "done")
|
||||
|
||||
def test_fault_spec_dispatches_hunt(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
ship_dir = _create(root, "add-export-button", "short", stop_at="verified")
|
||||
(ship_dir / "spec.md").write_text(FAULT_SPEC, encoding="utf-8")
|
||||
_fill_task(ship_dir)
|
||||
doc = _next(root, "add-export-button", "short")
|
||||
self.assertEqual(doc["step"], "hunt")
|
||||
self.assertEqual(doc["skill"], "hunt")
|
||||
|
||||
def test_long_starts_with_specifier_worker(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
_create(root, "tenant-permissions", "long")
|
||||
doc = _next(root, "tenant-permissions", "long")
|
||||
self.assertEqual(doc["step"], "specifier_new")
|
||||
self.assertEqual(doc["skill"], "specifier")
|
||||
self.assertEqual(doc["actor"], "worker")
|
||||
|
||||
def test_pr_check_then_merge(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
ship_dir = _create(root, "pr-export", "short", stop_at="released")
|
||||
_patch_current(
|
||||
ship_dir,
|
||||
source={"kind": "pr", "ref": "https://git.example.com/app/-/merge_requests/12"},
|
||||
)
|
||||
data = safe_load((ship_dir / "current.yaml").read_text(encoding="utf-8"))
|
||||
data["evidence"]["prs"] = [
|
||||
{"url": "https://git.example.com/app/-/merge_requests/12", "status": "opened"}
|
||||
]
|
||||
(ship_dir / "current.yaml").write_text(safe_dump(data), encoding="utf-8")
|
||||
doc = _next(root, "pr-export", "pr")
|
||||
self.assertEqual(doc["step"], "check_pr")
|
||||
self.assertEqual(doc["skill"], "check")
|
||||
|
||||
_patch_current(
|
||||
ship_dir,
|
||||
manager={"lastStep": "check_pr", "lastResult": "pass", "inFlight": None},
|
||||
)
|
||||
doc = _next(root, "pr-export", "pr")
|
||||
self.assertEqual(doc["step"], "merge_pr")
|
||||
self.assertEqual(doc["skill"], "delivery-loop")
|
||||
|
||||
def test_failed_worker_blocks_manager(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
ship_dir = _create(root, "add-export-button", "short", stop_at="verified")
|
||||
(ship_dir / "spec.md").write_text(FILLED_SPEC, encoding="utf-8")
|
||||
_fill_task(ship_dir)
|
||||
_patch_current(
|
||||
ship_dir,
|
||||
manager={"lastStep": "implement", "lastResult": "fail", "inFlight": None},
|
||||
)
|
||||
doc = _next(root, "add-export-button", "short")
|
||||
self.assertEqual(doc["status"], "blocked")
|
||||
self.assertIn("不要自己做", doc["summary"])
|
||||
|
||||
def test_entry_skills_are_managers(self) -> None:
|
||||
for name in ("short-ship", "long-ship", "pr-ship"):
|
||||
text = (REPO_ROOT / "skills" / name / "SKILL.md").read_text(encoding="utf-8")
|
||||
self.assertIn("开新 agent", text)
|
||||
self.assertNotIn("读取并执行", text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user