33 Commits

Author SHA1 Message Date
laily 44cee4be91 feat(fixds): 增加「用户在赶时间」硬规则
用户默认在赶时间:每个动作、每次追问、每段输出都要能缩短拿到结果的时间。
先给结论再给背景、独立命令一次发出、能自查的不问用户、同一事实不重复、
无关线索不展开、同一路径两轮无产出就换方法。

同时划清边界:省掉的是多余动作与多余追问,不是必要确认——要求与实测冲突、
越界改动、破坏性操作仍须先问。规则编号顺延为 1-7,README 与案例文件的
判定标准同步更新。
2026-09-19 14:09:02 +08:00
laily 146182c1b9 feat(fixds): 新增 DeepSeek Flash 模型行为约束 skill
把一次会话复盘中确认的浪费动作固化为硬规则,面向 DeepSeek Flash /
DeepSeek V4.1 Flash:先验证再实现、搜索必须有边界、只在被要求的范围内动作
(改项目外目录先问)、命令一次写对、测试按影响面、要求与实测冲突时报告并
等待确认而不擅自改需求。

不设运行时模型判定,只在 description 与正文标记适用范围,避免留出「模型标识
不匹配就不遵循」的逃生口。真实案例与代价放在 references/anti-patterns.md。
2026-09-19 14:05:15 +08:00
laily e60755f280 fix(builder): 版本时间格式改回 YYYYMMDDTHHMM
按要求保留字母分隔符:20260918T1722。分钟精度不变,同一分支同日多次
构建仍能正确排序。

记录切换边界:Debian 里字母排在标点之前,YYYYMMDDT… 小于旧格式的
YYYYMMDD.…,切换当天同日的旧格式包会压住新包,需要在仓库里把那些旧包
移除或停用;次日新格式始终更大。回归测试显式断言这行边界。
2026-09-19 13:32:16 +08:00
laily eb2d0ce651 fix(builder): 版本时间精确到分钟,修复同日重构建的 apt 排序
同日同分支的测试版本前缀完全相同,只剩 +g<sha> 可区分;Debian 版本比较把
hex 当普通文本,新构建可能小于旧构建,apt upgrade 因此拿不到最新包。

build_date() → build_stamp(),改为 YYYYMMDDHHMM,时间与日期拼成一段数字
才能按数值比较。不能写成 20260918T1722:Debian 里字母排在标点之前,带 T
的版本小于同日旧格式包的 20260918.…,修复当天仍抢不过已上传的旧包。

补回归测试,用 dpkg --compare-versions 锁定同日重建更大、且压过旧格式同日包。
2026-09-19 13:23:52 +08:00
laily b476176f3b Merge branch 'main' of https://git.yumee.top/laily/.pouch 2026-09-16 23:09:21 +08:00
laily cc9af04a40 tester v2: 术语对齐 pytest/JUnit/Robot,补齐 automation 与 not-run
字段改名(每项取舍的理由见新增的 references/concepts.md):
- 用例 status -> lifecycle, suite -> tag, surface -> driver,
  cleanup -> teardown, skip_unless -> assume;删除 requires
- risk 取值 safe/act/harm -> readonly/mutating/destructive(单调三级,
  与 gate 正交)
- catalog auth_env -> authorize;授权变量统一为
  AUTHORIZE_MUTATING / AUTHORIZE_DESTRUCTIVE

新增用例字段 automation(automated/assisted/manual),默认 automated,
并在 validate 里机器校验:manual 禁用 driver 与 how.run 且必须有
how.steps;assisted 至少一步以 [人工] 开头。select 新增 --automation。

结果词新增 not-run,区分「主动不跑」(skipped:retired/pending/超风险
上限/未授权/assume 不成立)与「没轮到」(失败即停后、被分类前置挡住整组、
需人工而无人值守)。

status_probe 定为分类级前置条件:catalog 顶层为默认,categories[] 同名键
覆盖(缺省继承 / false 关闭 / mapping 整体替换)。select 按分类返回生效值,
并把 probe.url 用 vars 展开(solve Agent 无法自行解析 ${VAR} 的问题),
同批返回 vars 供 how 里的 ${...} 替换。

新增 references/concepts.md:术语表(每词一句唯一定义 + 业界锚点 +
反例)、v1->v2 映射、以及 risk/automation/not-run/status_probe 的设计理由。
其余文档(SKILL/README/schema/levels/run/maintain)与三个脚本(catalog、
generate_cases_csv、generate_run_csv)同步更新。
2026-09-16 23:07:08 +08:00
laily d791f5584c refactor(releaser): 将 manage-release 改名为 releaser
- skills/manage-release -> skills/releaser(git mv,内容不变)
- releaser 的 frontmatter/标题改名,description 保留旧名作为触发词
- skills/manage-release/ 保留为纯弃用占位(SKILL.md + README.md,指向 releaser),
  已装软链与旧项目 .pouch.yaml 不再悬空或失效
- 同步或引用:AGENTS.md、README.md、docs/getting-started.md、orc(routing/SKILL/README)、
  ack delivery、builder(SKILL/README/contract)、memory-distill、pouch onboard 与
  suggest_skills
- 测试:catalog 需列出旧名占位,新增弃用占位断言,orc children 纳入旧名占位
2026-09-16 15:14:12 +08:00
laily ca3156ed93 Merge branch 'main' of https://git.yumee.top/laily/.pouch 2026-09-16 14:40:34 +08:00
laily bdfd2ff342 feat: update tester 2026-09-16 14:40:33 +08:00
laily ca090cd7f2 feat: update skills 2026-09-15 12:29:40 +08:00
laily 2783946cec Merge branch 'main' of https://git.yumee.top/laily/.pouch 2026-09-10 17:12:11 +08:00
laily a47e899702 feat(tester): add catalog-driven black-box test skill
Maintain and run project test catalogs under .pouch/tester, filtered by
category, id, and risk (safe/act/harm). Scripts generate the human CSV
from cases YAML.
2026-09-10 16:34:38 +08:00
laily 2ba027098f feat(builder): Android/iOS 构建与 copy_artifact 发布
新增 APK/IPA 轨道和 copy_artifact.sh(本地目录或 container:/dir)。
push 必须委托该脚本,禁止在 makefile 里内联 docker cp。
2026-09-09 01:13:56 +08:00
laily aa7a265e76 feat(builder): declare multi-arch builds with ARCHES
Project architecture lists belong in makefile.builder, not .env.builder.
deb/push-deb fan out when ARCH is omitted; Docker platforms derive from
ARCHES. Legacy makefiles without ARCHES keep the old single-arch behavior.
2026-09-08 16:31:53 +08:00
laily c248c3f0e3 feat: update readme 2026-09-08 15:40:22 +08:00
laily 9b3e16cb63 feat: update readme 2026-09-08 15:25:58 +08:00
laily 06865939e1 fix(builder): bump test versions to next patch
After a stable tag vX.Y.Z, untagged commits now produce
X.Y.(Z+1)~date.branch.n+gSHA so test DEBs sort above the last
official release and still below the next official X.Y.(Z+1).
2026-09-08 14:30:49 +08:00
laily 852dee3a69 feat(builder): append YYYYMMDD to test versions 2026-09-08 10:59:17 +08:00
laily 83deb886b5 feat(deployer): fall back to scp when rsync is missing 2026-09-07 15:43:38 +08:00
laily 17643bc17b feat: add eli5
Port anthropics/claude-plugins-community eli5 into builtin skills as a
single-page HTML picture explainer for readers with no background.
2026-09-04 23:15:03 +08:00
laily 4ce08ec89e Merge branch 'main' of https://git.yumee.top/laily/.pouch 2026-09-04 20:10:00 +08:00
laily b28acc66d8 feat: add host-sec-check 2026-09-04 11:54:07 +08:00
laily c2d6a5a0d7 feat(deployer): skip sync on localhost and complete skill READMEs
Redeploy locally with docker compose when the node is 127.0.0.1 and
the image already exists. Make list/sync/remote/deb importable under
python3 -I -S. Add human README.md for every builtin skill, slim ack's
user-facing docs, and drop unused OpenSpec config.
2026-09-04 00:07:15 +08:00
laily ef22c6829e refactor(skills): slim ack/builder/deployer for layered loading
Move mode-specific steps into references so SKILL.md only keeps routing and fail-closed rules.
2026-08-26 11:26:58 +08:00
laily 2be0964d73 feat(pouch): add skill structure audit to cut token waste
Teach pouch to optimize a named skill's loading layout: keep SKILL.md as a
router, move mode-specific steps to references, and measure footprint with
an audit script instead of dumping the whole skill into context.
2026-08-26 10:36:24 +08:00
laily 681aa9e237 feat(builder): load publish credentials from .env.builder
Keep DEB/Docker publish keys out of the project's .env. Scripts and
check.py --ready only read .env.builder; empty values count as missing.
2026-08-25 16:58:56 +08:00
laily 10d8800f07 feat: add skill init/check and isolate builder makefile
Give ack, builder, and deployer an explicit init/check mode that reports
missing project config instead of failing mid-work. Point builder at
makefile.builder so its contract targets do not collide with an existing
Makefile.
2026-08-25 16:49:28 +08:00
laily e9d2b5fde6 fix(builder): point version.mk at ~/.pouch after merging main
Keep ~/.skills as a lookup fallback so existing checkouts still resolve
version.sh.
2026-08-25 15:29:08 +08:00
laily f0b71b5bec Merge remote-tracking branch 'origin/main' into rename 2026-08-25 15:27:32 +08:00
laily 4423e7df1a feat: update 2026-08-25 15:26:29 +08:00
laily 12e00bd594 Merge branch 'main' into rename
Keep pouch naming and .pouch/ack project state, and bring in ACK
regression mode, deployer test-environment binding, and manage-release
updates from main.
2026-08-25 15:23:48 +08:00
laily f3cd56b78e feat: rename skills/skiff to pouch and move ACK state under .pouch
Use ~/.pouch, the pouch CLI, and .pouch.yaml as the SSOT container.
Keep the inner skills/ packages, and store ACK project state in
.pouch/ack instead of docs/ack.
2026-08-25 15:20:02 +08:00
laily 03d6298145 Merge pull request 'feat: update release' (#7) from fix-release into main
Reviewed-on: laily/.skills#7
2026-08-25 14:42:58 +08:00
171 changed files with 9886 additions and 2928 deletions
+2 -1
View File
@@ -1,4 +1,4 @@
# Python(skiff CLI 运行时产物)
# Python(pouch CLI 运行时产物)
__pycache__/
*.py[cod]
*$py.class
@@ -31,3 +31,4 @@ Thumbs.db
# 本地环境配置(勿提交密钥)
.env
.env.*
.pouch
@@ -1,4 +1,4 @@
# 复制为 docs/ack/knowledge.yaml,替换占位符。结构见 templates/knowledge.schema.json。
# 复制为 .pouch/ack/knowledge.yaml,替换占位符。结构见 templates/knowledge.schema.json。
# Developer/Test 只能在任务证据中提出 candidate;只有 Coordinator 写入这里。
version: 1
updatedAt: "2026-08-01T12:43:06+08:00"
@@ -1,6 +1,6 @@
# Agent Skills 仓库 ACK 协作协议(项目覆盖层)
> 本项目基于 ACK Skill v0.18.0。通用规范由 `/ack` 从 Skill 自身的
> 本项目基于 ACK Skill v0.19.0。通用规范由 `/ack` 从 Skill 自身的
> `references/` 读取;本文件只保存当前仓库差异。
## 项目概览
@@ -9,9 +9,9 @@
- 技术栈:Python 3、Markdown、JSON Schema
- 运行命令:`python3 -m unittest discover -s tests -p 'test_*.py'`
- Base URL:`n/a`
- 任务板:`docs/ack/tasks.yaml`
- 项目知识:`docs/ack/knowledge.yaml`
- 覆盖层文件:`docs/ack/project.md`
- 任务板:`.pouch/ack/tasks.yaml`
- 项目知识:`.pouch/ack/knowledge.yaml`
- 覆盖层文件:`.pouch/ack/project.md`
## 通用规范(由 ACK Skill 按需读取)
@@ -26,7 +26,7 @@
## Worker 路由
机器可校验的 worker profile、允许 worktree 和 receipt 只以
`docs/ack/tasks.yaml` 为准。本次 Developer 使用当前项目根 `/home/ace/.skills`;Test
`.pouch/ack/tasks.yaml` 为准。本次 Developer 使用当前项目根 `/home/ace/.skills`;Test
必须使用 fresh worker,并在独立临时项目、独立假 CLI 配置目录中完成黑盒演练。
## 路径权限
@@ -34,7 +34,7 @@
| 路径 | Coordinator | Test | Developer | 说明 |
|------|:-----------:|:----:|:---------:|------|
| `docs/ack-feishu-bug-intake.md` | R/W | Read-only | Read-only | 本需求规格与验收契约 |
| `docs/ack/` | R/W | Read-only | Read-only | ACK 项目状态,只有 Coordinator 写 |
| `.pouch/ack/` | R/W | Read-only | Read-only | ACK 项目状态,只有 Coordinator 写 |
| `skills/ack/` | Read-only | Read-only | R/W | ACK Skill 实现、模板与通用规范 |
| `tests/test_ack_feishu_intake_unit.py` | Read-only | Read-only | R/W | Developer 白盒测试 |
| `tests/test_ack_feishu_intake_e2e.py` | Read-only | R/W | Read-only | Test 独立黑盒演练与回归测试 |
@@ -57,8 +57,8 @@ Test 黑盒复测:
```bash
python3 -m unittest discover -s tests -p 'test_ack_feishu_intake_e2e.py'
python3 -m unittest discover -s tests -p 'test_ack*.py'
python3 skills/ack/scripts/validate_tasks.py docs/ack/tasks.yaml
python3 skills/ack/scripts/validate_knowledge.py docs/ack/knowledge.yaml --tasks docs/ack/tasks.yaml
python3 skills/ack/scripts/validate_tasks.py .pouch/ack/tasks.yaml
python3 skills/ack/scripts/validate_knowledge.py .pouch/ack/knowledge.yaml --tasks .pouch/ack/tasks.yaml
```
## 本次任务硬规则
@@ -1,14 +1,14 @@
version: 1
updatedAt: "2026-08-23T20:30:26+08:00"
source: "Coordinator (PM) Agent"
ackVersion: "0.18.0"
ackVersion: "0.19.0"
project:
name: "skills"
repoPath: "/home/ace/orca/workspaces/.skills/record-bug"
baseUrl: "n/a"
devWorktree: "/home/ace/orca/workspaces/.skills/record-bug"
overlayFile: "docs/ack/project.md"
knowledgeFile: "docs/ack/knowledge.yaml"
overlayFile: ".pouch/ack/project.md"
knowledgeFile: ".pouch/ack/knowledge.yaml"
orchestration:
profileVersion: 1
mode: "orca"
@@ -455,7 +455,7 @@ tasks:
- "python3 -m unittest discover -s tests -p 'test_ack_feishu_intake_unit.py'"
- "python3 -m unittest discover -s tests -p 'test_ack_feishu_intake_e2e.py'"
- "python3 -m unittest discover -s tests -p 'test_ack*.py'"
- "python3 skills/ack/scripts/validate_tasks.py docs/ack/tasks.yaml"
- "python3 skills/ack/scripts/validate_tasks.py .pouch/ack/tasks.yaml"
browser:
page: "n/a"
checks:
@@ -507,7 +507,7 @@ tasks:
explicit tenant-b selection, trusted executable resolution,
screenshot containment, stable opaque sourceRef deduplication and
fail-closed handling for malformed, ambiguous, non-progressing and
over-limit responses. Validators, skiff check, compileall and diff
over-limit responses. Validators, pouch check, compileall and diff
checks also passed.
resolution:
@@ -617,7 +617,7 @@ tasks:
verification:
commands:
- "python3 -m unittest tests.test_ack_omp_worker tests.test_ack_worker_profiles tests.test_ack_launch_worker"
- "python3 skills/ack/scripts/validate_tasks.py docs/ack/tasks.yaml"
- "python3 skills/ack/scripts/validate_tasks.py .pouch/ack/tasks.yaml"
browser:
page: "n/a"
checks:
@@ -809,8 +809,8 @@ tasks:
verification:
commands:
- "python3 -m unittest discover -s tests -p 'test_ack_omp_e2e.py'"
- "python3 skills/ack/scripts/validate_tasks.py docs/ack/tasks.yaml"
- "python3 skills/ack/scripts/validate_knowledge.py docs/ack/knowledge.yaml --tasks docs/ack/tasks.yaml"
- "python3 skills/ack/scripts/validate_tasks.py .pouch/ack/tasks.yaml"
- "python3 skills/ack/scripts/validate_knowledge.py .pouch/ack/knowledge.yaml --tasks .pouch/ack/tasks.yaml"
browser:
page: "n/a"
checks:
+101 -100
View File
@@ -1,30 +1,27 @@
# Agent Skills 仓库
# pouch
自研 Agent Skills 与配套规范资料的单一事实来源(SSOT)。Skill 内容、规范包与可运行的 skiff CLI 在本仓库一并维护。
自研 Agent Skills 与配套规范资料的单一事实来源(SSOT)。仓库名、家目录 `~/.pouch` 和 CLI 都叫 **pouch**。旧名是 `skills` / `pouch`。
| 仓库 | 地址 | 职责 |
| --------------- | ------------------------------------------------------------------------ | -------------------------- |
| **skills**(本仓库) | [https://git.yumee.top/laily/skills](https://git.yumee.top/laily/skills) | skill、规范包、skiff CLI 的当前 SSOT |
| **skiff** | [https://git.yumee.top/laily/skiff](https://git.yumee.top/laily/skiff) | skiff CLI 的独立来源 / 上游同步参考 |
| **pouch**(本仓库) | [https://git.yumee.top/laily/pouch](https://git.yumee.top/laily/pouch) | skill、规范包、pouch CLI 的当前 SSOT |
---
## 快速开始
需要 Python 3、git,以及 Cursor / Claude Code / Codex 或其它兼容 Agent Skills 的 Agent。人类用法见根 [README.md](README.md) 和各 skill 的 `README.md`。用户要代装时:pouch skill 已安装则走其「安装 pouch」模式;否则读 [skills/pouch/references/install.md](skills/pouch/references/install.md) 并执行。不要只用 `pouch select`。
```bash
# 1. 克隆并关联
git clone https://git.yumee.top/laily/skills.git ~/.skills
cd ~/.skills && ./install.sh
git clone https://git.yumee.top/laily/pouch.git ~/.pouch
cd ~/.pouch && ./install.sh
# install.sh 会自动把 skiff 项目 skill 安装到所有 Agent
# 2. 安装其他 skill
skiff add declarative-openspec-loop -g
# 3. 查看状态
skiff list
skiff status
# install.sh 只把 pouch skill 装到所有 Agent;其它 skill 用 select / add
pouch select -g
pouch list
pouch status
```
---
@@ -32,22 +29,14 @@ skiff status
## 仓库结构
```
skills/
├── _template/ # 新建 skill 的脚手架
├── ack/ # 完整 ACK skill,含 references/templates/scripts
├── declarative-openspec-loop/ # 自研 skill,必须含 SKILL.md
│ ├── SKILL.md
│ └── reference.md
├── discussion-notes/ # 讨论沉淀笔记
│ ├── SKILL.md
│ └── reference.md
skiff/ # CLI 源码(Python 3)
bin/skiff # CLI 入口
catalog.yaml # skiff 预置 Skill 来源目录
AGENTS.md # 本文档
skills/ # 自研 skill(SSOT):每个子目录含 SKILL.md(Agent)和 README.md(人类)
pouch/ # CLI 源码(Python 3)
bin/pouch # CLI 入口
catalog.yaml # pouch 预置 Skill 来源目录
AGENTS.md # 本文档
```
**本仓库包含**:`skills/`、`skiff/`、`bin/skiff`、`catalog.yaml`、`AGENTS.md`
**本仓库包含**:`skills/`、`pouch/`、`bin/pouch`、`catalog.yaml`、`AGENTS.md`
**本仓库不包含**:各项目的 skill 启用清单
---
@@ -57,28 +46,43 @@ AGENTS.md # 本文档
### 内置(Builtin)
| Skill | 说明 |
| ---------------------------------------------------------------------- | ------------------------------------------------- |
| [orc](skills/orc/SKILL.md) | ORC 入口:显式编排开发、版本发布与产物任务,支持 Agent 分档 |
| [ack](skills/ack/SKILL.md) | ACK 入口:显式初始化、检查并运行三角色闭环、测试环境、发版与回归 |
| [skiff](skills/skiff/SKILL.md) | 本项目工作流:创建、使用、反馈与更新 builtin skill |
| [declarative-openspec-loop](skills/declarative-openspec-loop/SKILL.md) | 声明式编程循环:用户提供校验方式,Agent 自动 propose/apply/校验并迭代直到通过 |
| [discussion-notes](skills/discussion-notes/SKILL.md) | 讨论沉淀:边讨论边维护 Markdown 笔记,无 .raw.md |
| Skill | 说明 |
| --- | --- |
| [ack](skills/ack/README.md) | 显式初始化、检查并运行三角色闭环:需求 / bug、测试环境、发版与回归 |
| [builder](skills/builder/README.md) | 按统一契约构建和发布 DEB / Docker 镜像 |
| [declarative-openspec-loop](skills/declarative-openspec-loop/README.md) | 你给校验方式,Agent 自动 propose / apply / 校验直到通过 |
| [deployer](skills/deployer/README.md) | 用 Git 里的 Compose 或 Argo CD 清单部署服务 |
| [discussion-notes](skills/discussion-notes/README.md) | 边讨论边维护 Markdown 笔记 |
| [draw-prototype-flow](skills/draw-prototype-flow/README.md) | 把产品想法画成可编辑的 Draw.io 低保真原型和用户动线 |
| [eli5](skills/eli5/README.md) | 用大图少字的单页 HTML 把话题讲给完全没背景的人听 |
| [host-sec-check](skills/host-sec-check/README.md) | 对 Linux/VPS 做入侵取向的安全检查,默认只出报告 |
| [memory-distill](skills/memory-distill/README.md) | 任务结束后把可复用结论写入通用记忆和项目记忆 |
| [note-style](skills/note-style/README.md) | 把材料整理成可长期查阅的知识笔记 |
| [orc](skills/orc/README.md) | 把开发、发版、DEB / Docker 意图编排成阶段,按档位分发给对应 skill |
| [pouch](skills/pouch/README.md) | 创建、校验、安装和维护 builtin skill |
| [releaser](skills/releaser/README.md) | 给已准备好的 commit 发版本:更新说明、tag、可选 Forge Release |
| [manage-release](skills/manage-release/README.md) | 已弃用:`releaser` 的旧名兼容占位,不含流程 |
改名后旧名保留一个纯占位 skill,避免已装软链悬空和旧项目 `.pouch.yaml` 失效。确认没有
项目再按 `manage-release` 启用后,删除 `skills/manage-release/` 即可,不丢任何内容。
新建 skill:复制 `skills/_template/` → `skills/<name>/`,编辑 `SKILL.md`,在本仓库 commit。
新建 skill:优先 `pouch create`;或复制 `skills/_template/` → `skills/<name>/`,编辑 `SKILL.md` 和 `README.md`,在本仓库 commit。
Skill 需要的稳定规范、模板、示例和脚本直接放在自己的目录中,例如
`skills/ack/references/`、`templates/`、`examples/` 和 `scripts/`。项目初始化只生成
项目状态,不复制或链接 Skill 内容:
```bash
skiff init ack
pouch init ack
```
builder 与 deployer 的项目接入走各自 skill 的「初始化」模式(检查配置并引导补齐),
不要 `pouch init builder` / `pouch init deployer`。
### 预置目录(Catalog)
在 `catalog.yaml` 中预置,通过 skiff 拉取安装:
在 `catalog.yaml` 中预置,通过 pouch 拉取安装:
| Source | 来源 |
@@ -87,8 +91,8 @@ skiff init ack
```bash
skiff fetch waza
skiff add waza/think -g
pouch fetch waza
pouch add waza/think -g
```
### 自定义仓库(Custom Sources)
@@ -96,21 +100,21 @@ skiff add waza/think -g
公司或团队维护、且一个仓库中包含多个 skill 时,使用命名 custom source:
```bash
skiff source add company \
pouch source add company \
git@git.company.com:platform/agent-skills.git \
--skills-path skills
skiff list --source company
skiff add company/internal-review -g
pouch list --source company
pouch add company/internal-review -g
```
也可以接入已有本地 checkout:
```bash
skiff source add company --local ~/code/company-skills --skills-path skills
pouch source add company --local ~/code/company-skills --skills-path skills
```
配置保存在 `~/.config/skiff/config.yaml`,Git source 默认 clone 到
`~/.local/share/skiff/sources/<source>/`。项目 `.skills.yaml` 只记录逻辑
配置保存在 `~/.config/pouch/config.yaml`,Git source 默认 clone 到
`~/.local/share/pouch/sources/<source>/`。项目 `.pouch.yaml` 只记录逻辑
source 名称,每台机器独立配置实际仓库地址。
### 社区(External NPM / GitHub)
@@ -127,28 +131,28 @@ npx skills find typescript
## 设计原则
1. **SSOT** — 自研 skill 只存在于 `skills/<name>/`,不在 Agent 目录直接创建
2. **项目自治** — 每个项目自己维护 `.skills.yaml`,本仓库不维护项目清单
2. **项目自治** — 每个项目自己维护 `.pouch.yaml`,本仓库不维护项目清单
3. **软链优先** — 通过 symlink 映射到 Agent 目录,改 skill 即改 SSOT
4. **能力内聚** — Skill 使用的规范、模板和脚本与 `SKILL.md` 同目录维护
5. **一体维护** — skill 与 CLI 同仓库维护;需要时再与独立 skiff 仓库同步
5. **一体维护** — skill 与 CLI 同仓库维护
---
## 架构
```
skills 仓库(本仓库) skiff CLI
skills/<name>/ ←── skiff install / enable
skiff/ ←── python3 -m skiff
catalog.yaml ←── skiff add / fetch
pouch 仓库(本仓库) pouch CLI
skills/<name>/ ←── pouch install / enable
pouch/ ←── python3 -m pouch
catalog.yaml ←── pouch add / fetch
↑
~/.skills(symlink)
~/.pouch(symlink)
│
┌────┴────┐
▼ ▼
~/.cursor/skills/ project/.agents/skills/
~/.claude/skills/ project/.claude/skills/
~/.codex/skills/ project/.skills.yaml
~/.codex/skills/ project/.pouch.yaml
~/.agents/skills/ (agents 标准目录,覆盖 OMP)
```
@@ -158,16 +162,10 @@ catalog.yaml ←── skiff add / fetch
| 层级 | 位置 | 维护方式 |
| ---------------- | ---------------------------------- | ------------------------------- |
| **Builtin** | `skills/<name>/` | 本仓库 commit |
| **Catalog** | `catalog.yaml` + checkout 缓存 | `skiff catalog add / fetch` |
| **Custom Source** | `~/.local/share/skiff/sources/` 或本地路径 | `skiff source add/fetch` |
| **Catalog** | `catalog.yaml` + checkout 缓存 | `pouch catalog add / fetch` |
| **Custom Source** | `~/.local/share/pouch/sources/` 或本地路径 | `pouch source add/fetch` |
| **External NPM** | `node_modules/` | `npx skills add` / `skills-npm` |
### 非 Skill 资料分类
| 类型 | 位置 | 维护方式 |
| --- | --- | --- |
| **OpenSpec 配置** | `openspec/` | 本仓库 commit;服务于本仓库自身的规格流程 |
### 多 Agent 路径
@@ -184,10 +182,10 @@ catalog.yaml ←── skiff add / fetch
## 项目级启用
每个项目**自己维护** `.skills.yaml`,不由本仓库管理:
每个项目**自己维护** `.pouch.yaml`,不由本仓库管理:
```yaml
# .skills.yaml(在项目根目录)
# .pouch.yaml(在项目根目录)
skills:
- name: declarative-openspec-loop
source: builtin
@@ -205,38 +203,38 @@ targets: # 可选,默认 all
| 概念 | 类比 |
| -------------- | --------------------------- |
| skills 仓库 | npm registry |
| `.skills.yaml` | `package.json` dependencies |
| `skiff enable` | `npm install` |
| `skiff sync` | `npm ci` |
| pouch 仓库 | npm registry |
| `.pouch.yaml` | `package.json` dependencies |
| `pouch enable` | `npm install` |
| `pouch sync` | `npm ci` |
项目级命令:
```bash
cd ~/code/my-app
skiff enable declarative-openspec-loop
skiff disable declarative-openspec-loop
skiff sync
pouch enable declarative-openspec-loop
pouch disable declarative-openspec-loop
pouch sync
```
---
## skiff 命令
## pouch 命令
详见 [skiff README](https://git.yumee.top/laily/skiff)。
详见 [pouch README](https://git.yumee.top/laily/pouch)。
### 已实现
| 命令 | 说明 |
| -------------------------------------- | ------------------- |
| `skiff bootstrap` | 将 skiff 项目 skill 全局安装到所有 Agent |
| `skiff list` | 列出所有 skill |
| `skiff status` | 安装状态总览 |
| `skiff add <name> [-g]` | 项目或全局安装(symlink) |
| `skiff remove <name> [-g]` | 移除 symlink |
| `skiff catalog add` / `skiff fetch` | 管理和拉取 catalog source |
| `pouch bootstrap` | 将 pouch 项目 skill 全局安装到所有 Agent |
| `pouch list` | 列出所有 skill |
| `pouch status` | 安装状态总览 |
| `pouch add <name> [-g]` | 项目或全局安装(symlink) |
| `pouch remove <name> [-g]` | 移除 symlink |
| `pouch catalog add` / `pouch fetch` | 管理和拉取 catalog source |
### 草稿与健康检查
@@ -244,10 +242,10 @@ skiff sync
| 命令 | 说明 |
| --- | --- |
| `skiff create <name> --idea TEXT [--from-project PATH]` | 从 `_template/` 创建草稿 |
| `skiff check <name>` | 校验草稿或正式 skill |
| `skiff finalize <name>` | 校验并将草稿转为正式 skill |
| `skiff doctor [--fix]` | symlink 健康检查与修复 |
| `pouch create <name> --idea TEXT [--from-project PATH]` | 从 `_template/` 创建草稿 |
| `pouch check <name>` | 校验草稿或正式 skill |
| `pouch finalize <name>` | 校验并将草稿转为正式 skill |
| `pouch doctor [--fix]` | symlink 健康检查与修复 |
---
@@ -258,7 +256,8 @@ skiff sync
```
skill-name/
├── SKILL.md # 必需
├── SKILL.md # 必需,给 Agent
├── README.md # 必需,给人类
├── reference.md # 可选
├── examples.md # 可选
└── scripts/ # 可选
@@ -282,12 +281,12 @@ description: >-
### 新建流程
1. `skiff create my-skill --idea "..." --from-project .`
2. Agent 编辑 `~/.skills/.drafts/my-skill/SKILL.md`
3. `skiff check my-skill`
4. 用户确认后执行 `skiff finalize my-skill`
5. `skiff add my-skill -a cursor -g -y` 验证
6. 在本仓库 commit;需要的项目再用 `skiff add my-skill` 启用
1. `pouch create my-skill --idea "..." --from-project .`
2. Agent 编辑 `~/.pouch/.drafts/my-skill/SKILL.md` 和 `README.md`
3. `pouch check my-skill`
4. 用户确认后执行 `pouch finalize my-skill`
5. `pouch add my-skill -a cursor -g -y` 验证
6. 在本仓库 commit;需要的项目再用 `pouch add my-skill` 启用
**禁止**在 `~/.cursor/skills/` 或项目 Agent 目录直接创建非 symlink 的 skill。
@@ -299,7 +298,7 @@ symlink 正确时,Agent 在项目里改 skill 文件 = 直接改 SSOT:
```
project/.agents/skills/foo/SKILL.md
→ ~/.skills/skills/foo/SKILL.md
→ ~/.pouch/skills/foo/SKILL.md
→ 在本仓库 commit
```
@@ -314,7 +313,7 @@ Claude Code 对 symlink 支持不稳定:可能无法发现 skill,或写入
| -------------- | ----------------------------------------------------- |
| Cursor / Codex | symlink,正常 |
| Claude Code | symlink 单个 skill 目录,不要 symlink 整个 `~/.claude/skills/` |
| symlink 被替换 | `skiff doctor --fix` → 重建 symlink |
| symlink 被替换 | `pouch doctor --fix` → 重建 symlink |
---
@@ -324,7 +323,7 @@ Claude Code 对 symlink 支持不稳定:可能无法发现 skill,或写入
| 场景 | 工具 |
| -------------- | ---------------------------------------------------- |
| 自研 skill 安装/管理 | **skiff** |
| 自研 skill 安装/管理 | **pouch** |
| 社区 skill 安装 | **Vercel `npx skills add`** |
| NPM 包内 skill | **skills-npm** / **skill-indexer** |
| 搜索发现 | **npx skills find** / [skills.sh](https://skills.sh) |
@@ -337,13 +336,15 @@ Claude Code 对 symlink 支持不稳定:可能无法发现 skill,或写入
| 我要… | 命令 | 在哪 |
| ---------- | --------------------------------- | ---- |
| 首次安装 | `git clone <repo> ~/.skills && ~/.skills/install.sh` | 任意 |
| 新建 skill | `skiff create` → Agent 完善 → `check/finalize` | 任意项目 |
| 全局启用 | `skiff install <name>` | 任意 |
| 项目启用 | `skiff enable <name>` | 项目目录 |
| 看状态 | `skiff status` | 任意 |
| 首次安装 | `git clone <repo> ~/.pouch && ~/.pouch/install.sh` | 任意 |
| 让 Agent 代装 | 说「帮我安装 pouch」或丢仓库 URL | 任意 |
| 介绍 / 接入 skill | 说「有哪些 skill」或「把这个项目该用的 skill 接上」 | 项目目录 |
| 新建 skill | `pouch create` → Agent 完善 → `check/finalize` | 任意项目 |
| 全局启用 | `pouch add <name> -g` | 任意 |
| 项目启用 | `pouch add <name>` 或 `pouch enable <name>` | 项目目录 |
| 看状态 | `pouch status` | 任意 |
| 装社区 skill | `npx skills add owner/repo -g -y` | 任意 |
| 更新外部 skill | `skiff fetch <name>` | 任意 |
| 更新外部 skill | `pouch fetch <name>` | 任意 |
---
@@ -351,7 +352,7 @@ Claude Code 对 symlink 支持不稳定:可能无法发现 skill,或写入
## 参考
- [Agent Skills 开放标准](https://agentskills.io)
- [skiff CLI](https://git.yumee.top/laily/skiff)
- [pouch CLI](https://git.yumee.top/laily/pouch)
- [Vercel skills CLI](https://github.com/vercel-labs/skills)
- [skills.sh](https://skills.sh)
- [Cursor Skills 文档](https://cursor.com/docs/context/skills)
+116 -53
View File
@@ -1,86 +1,146 @@
# Agent Skills
# pouch
自研 [Agent Skills](https://agentskills.io) 的单一事实来源(SSOT)。Skill 内容与 **skiff** CLI 在本仓库一并维护。
自研 [Agent Skills](https://agentskills.io) 的单一事实来源(SSOT)。仓库、家目录 `~/.pouch` 和 CLI 都叫 **pouch**。Skill 内容与 CLI 在本仓库一并维护。
pouch 本义是小袋子,这里取做锦囊妙计的锦囊:skill 就是里面的妙计。
给同事看怎么装、怎么打版本、怎么构建,读 [docs/getting-started.md](docs/getting-started.md)。人类用法看各 skill 的 `README.md`;`SKILL.md` 是给 Agent 的指令。用户要你代装时,读 [skills/pouch/references/install.md](skills/pouch/references/install.md) 并执行,不要只把命令贴回去,也不要用 `pouch select`。
## 前置
- Python 3、git
- Cursor、Claude Code、Codex,或其它兼容 Agent Skills 的 Agent
- `~/.local/bin` 需要在 `PATH` 里(`install.sh` 会写入 bash / zsh / fish 的 rc,当前 shell 要重开或 `source` 一次才生效)
## 快速开始
```bash
git clone https://git.yumee.top/laily/skills.git ~/.skills
cd ~/.skills
./install.sh # 安装 CLI,并将 skiff 项目 skill 安装到所有 Agent
git clone https://git.yumee.top/laily/pouch.git ~/.pouch
cd ~/.pouch
./install.sh
skiff add declarative-openspec-loop -g
skiff select # 交互式选择并批量安装
skiff list
skiff status
# install.sh 会安装 pouch 命令,并把 pouch 这一个 skill 装到所有 Agent
# 若当前 shell 找不到 pouch:重开终端,或 source 对应 rc
pouch select # 交互选择要装的 skill(默认当前项目)
pouch select -g # 或装到全局,所有项目都能用
pouch list
pouch status
```
从旧的 `~/.skills` / `pouch` 迁移:
```bash
mv ~/.skills ~/.pouch
~/.pouch/install.sh
```
`install.sh` 会安装 `pouch` 命令,并保留 `pouch` 作为旧命令别名。项目里已有的 `.skills.yaml` 仍可读取;新写入使用 `.pouch.yaml`。
## 让 Agent 代装
对 Agent 说「帮我安装 pouch」,或把仓库地址丢给它。步骤只维护在
[skills/pouch/references/install.md](skills/pouch/references/install.md):
确认 python3/git → 处理 `~/.pouch` → `install.sh` → 用短清单介绍 skill → 问要装哪些。
不要用 `pouch select`。不要在未确认时覆盖已有的 `~/.pouch`。
自己装仍看上面的「快速开始」。
## 装完之后怎么用
1. 打开已安装目标的 Agent(Cursor / Claude Code / Codex 等)。
2. 项目级安装:打开那个项目再说话。全局安装(`-g`):任意项目都可以。
3. 按对应 skill 的 README,把示例请求发给 Agent。
`./install.sh` 只全局安装 `pouch`。其它 skill 都要 `pouch add` 或 `pouch select`。
```bash
cd ~/code/my-app
pouch add ack # 当前项目
pouch add discussion-notes -g # 全局
```
## 仓库结构
```
skills/ # 自研 skill(SSOT):每个子目录必须有 SKILL.md
skiff/ # CLI 源码(Python 3)
bin/skiff # CLI 入口
catalog.yaml # skiff 预置 Skill 来源目录
skills/ # 自研 skill(SSOT):每个子目录含 SKILL.md 和 README.md
pouch/ # CLI 源码(Python 3)
bin/pouch # CLI 入口
catalog.yaml # pouch 预置 Skill 来源目录
AGENTS.md # 详细规范与架构说明
```
| 路径 | 说明 |
|------|------|
| [skills/](skills/) | 自研 skill,每个子目录含 `SKILL.md`,可附带 references、templates 和 scripts |
| [skiff/](skiff/README.md) | 安装、软链、健康检查 CLI |
| [catalog.yaml](catalog.yaml) | skiff 预置 Skill 来源目录 |
| [AGENTS.md](AGENTS.md) | 设计原则、编写规范、架构详解 |
| 路径 | 说明 |
| ---------------------------- | ----------------------------------------------------- |
| [skills/](skills/) | 自研 skill,每个子目录含给人看的 `README.md` 和给 Agent 的 `SKILL.md` |
| [pouch/](pouch/README.md) | 安装、软链、健康检查 CLI |
| [catalog.yaml](catalog.yaml) | pouch 预置 Skill 来源目录 |
| [AGENTS.md](AGENTS.md) | 设计原则、编写规范、架构详解 |
## 自研 Skill
| Skill | 说明 |
|-------|------|
| [orc](skills/orc/SKILL.md) | 显式编排开发、版本发布和产物任务,支持 low/mid/high Agent 档位 |
| [ack](skills/ack/SKILL.md) | 显式初始化、检查并运行 ACK 三角色协作及可选交付闭环 |
| [skiff](skills/skiff/SKILL.md) | 在项目中创建、安装、反馈和维护 builtin skill |
| [declarative-openspec-loop](skills/declarative-openspec-loop/SKILL.md) | 声明式编程循环:用户提供校验方式,Agent 自动迭代直到通过 |
| [discussion-notes](skills/discussion-notes/SKILL.md) | 讨论沉淀:边讨论边维护 Markdown 笔记 |
ACK 是包含规范、模板与校验脚本的完整 Skill。安装 Skill 后可初始化当前项目状态:
| Skill | 说明 |
| ----------------------------------------------------------------------- | ------------------------------------------- |
| [ack](skills/ack/README.md) | 显式初始化、检查并运行三角色闭环:需求 / bug、测试环境、发版与回归 |
| [builder](skills/builder/README.md) | 按统一契约构建和发布 DEB / Docker 镜像 |
| [declarative-openspec-loop](skills/declarative-openspec-loop/README.md) | 你给校验方式,Agent 自动 propose / apply / 校验直到通过 |
| [deployer](skills/deployer/README.md) | 用 Git 里的 Compose 或 Argo CD 清单部署服务 |
| [discussion-notes](skills/discussion-notes/README.md) | 边讨论边维护 Markdown 笔记 |
| [draw-prototype-flow](skills/draw-prototype-flow/README.md) | 把产品想法画成可编辑的 Draw.io 低保真原型和用户动线 |
| [eli5](skills/eli5/README.md) | 用大图少字的单页 HTML 把话题讲给完全没背景的人听 |
| [host-sec-check](skills/host-sec-check/README.md) | 对 Linux/VPS 做入侵取向的安全检查,默认只出报告 |
| [memory-distill](skills/memory-distill/README.md) | 任务结束后把可复用结论写入通用记忆和项目记忆 |
| [note-style](skills/note-style/README.md) | 把材料整理成可长期查阅的知识笔记 |
| [orc](skills/orc/README.md) | 把开发、发版、DEB / Docker 意图编排成阶段,按档位分发给对应 skill |
| [pouch](skills/pouch/README.md) | 创建、校验、安装和维护 builtin skill |
| [releaser](skills/releaser/README.md) | 给已准备好的 commit 发版本:更新说明、tag、可选 Forge Release |
| [manage-release](skills/manage-release/README.md) | 已弃用:`releaser` 的旧名兼容占位,不含流程 |
ACK 是包含规范、模板与校验脚本的完整 Skill。安装后可初始化当前项目状态:
```bash
skiff init ack
skiff init ack --project ~/app
pouch init ack
pouch init ack --project ~/app
```
初始化会生成默认关闭的 `docs/ack/delivery.yaml` 和空的 `docs/ack/regression.yaml`。
初始化会生成默认关闭的 `.pouch/ack/delivery.yaml` 和空的 `.pouch/ack/regression.yaml`。
项目可用自然语言让 `/ack` 把测试环境绑到 deployer、维护发版 profile,并在任务
验证通过后收获回归用例。
builder 与 deployer 的项目接入走各自 skill 的「初始化」模式,不要 `pouch init builder` 或 `pouch init deployer`。
新建 skill:
```bash
skiff create my-skill --idea "描述要解决的重复问题" --from-project .
# 由 Agent 完善 ~/.skills/.drafts/my-skill/SKILL.md
skiff check my-skill
skiff finalize my-skill
skiff add my-skill -g # 全局安装验证
pouch create my-skill --idea "描述要解决的重复问题" --from-project .
# 由 Agent 完善 ~/.pouch/.drafts/my-skill/SKILL.md 和 README.md
pouch check my-skill
pouch finalize my-skill
pouch add my-skill -g # 全局安装验证
```
项目里使用 skill 发现通用问题或优化时,让 Agent 按 `skiff` skill 收集实际结果与期望结果,修改 `~/.skills/skills/<name>/` 的 SSOT,并执行 `skiff check <name>`。项目专属规则保留在项目内,不回流到通用 skill。
项目里使用 skill 发现通用问题或优化时,让 Agent 按 `pouch` skill 收集实际结果与期望结果,修改 `~/.pouch/skills/<name>/` 的 SSOT,并执行 `pouch check <name>`。项目专属规则保留在项目内,不回流到通用 skill。
## 安装方式
### 全局(用户级)
```bash
skiff add <name> -g # 安装到 ~/.cursor/skills/ 等
skiff add <name> -g -a cursor
pouch add <name> -g # 安装到 ~/.cursor/skills/ 等
pouch add <name> -g -a cursor
```
### 项目级
在项目根目录维护 `.skills.yaml`:
在项目根目录维护 `.pouch.yaml`:
```yaml
skills:
- name: declarative-openspec-loop
- name: ack
source: builtin
- name: think
source: catalog:waza
@@ -92,9 +152,9 @@ targets: # 可选,默认 all
```
```bash
skiff add declarative-openspec-loop
skiff sync
skiff remove declarative-openspec-loop
pouch add ack
pouch sync
pouch remove ack
```
## Catalog 与 Custom Source
@@ -102,15 +162,15 @@ skiff remove declarative-openspec-loop
安装 catalog 中预置的来源:
```bash
skiff fetch waza
skiff add waza/think -g
pouch fetch waza
pouch add waza/think -g
```
接入团队自己的本地目录或 Git 仓库:
```bash
skiff source add company --local ~/code/company-skills --skills-path skills
skiff add company/internal-review -g
pouch source add company --local ~/code/company-skills --skills-path skills
pouch add company/internal-review -g
```
**社区来源**(Vercel CLI):
@@ -124,32 +184,34 @@ npx skills find typescript
```
本仓库
├── skills/<name>/ ←── skiff install / enable
├── skills/<name>/ ←── pouch add / select
├── catalog.yaml ←── 预置来源发现与 fetch
└── skiff/ ←── python3 -m skiff
└── pouch/ ←── python3 -m pouch
↑
~/.skills(symlink)
~/.pouch(symlink)
│
┌────┴────────────────┐
▼ ▼
~/.cursor/skills/ project/.agents/skills/
~/.claude/skills/ project/.claude/skills/
~/.codex/skills/ project/.skills.yaml
~/.codex/skills/ project/.pouch.yaml
~/.agents/skills/ (agents 标准目录,覆盖 OMP)
```
## 设计原则
1. **SSOT** — 自研 skill 只存在于 `skills/<name>/`
2. **项目自治** — 各项目自行维护 `.skills.yaml`
2. **项目自治** — 各项目自行维护 `.pouch.yaml`
3. **软链优先** — 通过 symlink 映射到 Agent 目录,改 skill 即改 SSOT
4. **能力内聚** — Skill 所需规范、模板和脚本与 `SKILL.md` 放在同一目录
5. **一体维护** — skill 与 CLI 同仓库,Python 3 直接运行,无需编译
## 文档
- [docs/getting-started.md](docs/getting-started.md):给同事的安装、打版本、构建教程
- [AGENTS.md](AGENTS.md) — 完整规范、多 Agent 路径、编写约定
- [skiff/README.md](skiff/README.md) — CLI 命令参考与开发说明
- [pouch/README.md](pouch/README.md) — CLI 命令参考与开发说明
- 各 skill 的人类说明见上表 `README.md`
## 参考
@@ -157,3 +219,4 @@ npx skills find typescript
- [Vercel skills CLI](https://github.com/vercel-labs/skills)
- [skills.sh](https://skills.sh)
- [Cursor Skills 文档](https://cursor.com/docs/context/skills)
+2 -2
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
# 跟随软链,确保从 ~/.local/bin/skiff 调用时仍能找到仓库根目录
# 跟随软链,确保从 ~/.local/bin/pouch 调用时仍能找到仓库根目录
SCRIPT="${BASH_SOURCE[0]}"
while [ -L "$SCRIPT" ]; do
link_dir="$(cd "$(dirname "$SCRIPT")" && pwd)"
@@ -11,4 +11,4 @@ done
REPO_ROOT="$(cd "$(dirname "$SCRIPT")/.." && pwd)"
export PYTHONPATH="${REPO_ROOT}${PYTHONPATH:+:$PYTHONPATH}"
exec python3 -m skiff "$@"
exec python3 -m pouch "$@"
+1 -1
View File
@@ -4,7 +4,7 @@
# repo: <git-url>
# ref: <branch|tag> (default: main)
# path: <subpath> (default: .)
# description: <text> (optional, shown by `skiff select`)
# description: <text> (optional, shown by `pouch select`)
# tags: (optional)
# - <tag>
#
+11 -11
View File
@@ -17,7 +17,7 @@ updated: "2026-07-31T23:31:48+08:00"
## 背景
本轮评审覆盖 `skills/ack/` 的入口、角色规范、闭环流程、项目模板、任务板
schema、校验脚本、Orca 适配器,以及 `skiff init ack` 的真实运行路径。
schema、校验脚本、Orca 适配器,以及 `pouch init ack` 的真实运行路径。
评审主要回答两个问题:
@@ -216,15 +216,15 @@ Deferred,当前 launcher 必须 fail closed。
## P0:初始化需要原子化
`skiff init ack` 会先写 `project.md` 和 `tasks.yaml`,再运行任务板校验。缺少 PyYAML
`pouch init ack` 会先写 `project.md` 和 `tasks.yaml`,再运行任务板校验。缺少 PyYAML
时,命令会失败但保留两个文件。再次运行又会因为拒绝覆盖而失败。
相关位置:
- `requirements.txt:1`
- `skills/ack/scripts/validate_tasks.py:37`
- `skiff/cli.py:1144`
- `skiff/cli.py:1156`
- `pouch/cli.py:1144`
- `pouch/cli.py:1156`
本轮在不含第三方包的隔离 Python 环境中复现了这个状态。
@@ -232,7 +232,7 @@ Deferred,当前 launcher 必须 fail closed。
- 在临时目录渲染和校验,全部通过后再原子 rename。
- 失败时只清理由本次调用创建的临时文件。
- 提供 `skiff init ack --repair` 或等价恢复路径。
- 提供 `pouch init ack --repair` 或等价恢复路径。
- 默认模板使用 `tasks: []`,完整示例继续放在 `examples/`。
- CLI 输出“脚手架已创建,待配置”,检查通过后再称为“初始化完成”。
@@ -332,7 +332,7 @@ ACK 默认不提交、不推送。隔离 worktree 中的任务即使复测通过
2. 把 Orca adapter 改成每轮 Developer/Test 双任务。
3. 定义同 worktree 默认策略和跨 worktree transfer。
4. 改造 worker launcher 与安全授权。
5. 原子化 `skiff init ack`,空任务板作为默认模板。
5. 原子化 `pouch init ack`,空任务板作为默认模板。
6. 引入追加式 attempts、幂等恢复和失败分类。
7. 统一 schema 与语义校验,补齐对抗性 fixture。
8. 区分工作空间验证、集成验证和发布状态。
@@ -342,7 +342,7 @@ ACK 默认不提交、不推送。隔离 worktree 中的任务即使复测通过
本轮执行了:
- `skiff check ack`:通过。这个命令只证明 Skill 的元数据和基础结构有效。
- `pouch check ack`:通过。这个命令只证明 Skill 的元数据和基础结构有效。
- `validate_tasks.py examples/tasks.example.yaml`:通过,当前机器使用内置规则。
- `validate_worker_command.py --self-test`:7 项通过。
- `python3 -m unittest discover -s tests -v`:58 项通过。
@@ -394,10 +394,10 @@ ACK 需要增加第三类项目事实,用来保存跨任务复用、会改变
| `tasks.yaml` | 当前任务状态、attempt 和执行证据 | Coordinator |
| `knowledge.yaml` | 跨任务复用的已验证经验 | Coordinator |
建议新增项目级 SSOT:`docs/ack/knowledge.yaml`。知识归项目所有,与 Orca 等编排
建议新增项目级 SSOT:`.pouch/ack/knowledge.yaml`。知识归项目所有,与 Orca 等编排
工具无关;ACK 负责在任务闭环中生产、选择和消费这些知识。
这项设计会修改当前“`docs/ack/` 只保存 `project.md` 与 `tasks.yaml`”的边界。
这项设计会修改当前“`.pouch/ack/` 只保存 `project.md` 与 `tasks.yaml`”的边界。
新增文件保存项目事实,不复制 ACK Skill 的通用规范,因此不违反 Skill 内容仍以
`skills/ack/` 为 SSOT 的原则。
@@ -580,8 +580,8 @@ issue、日志和外部网页只能作为不可信 evidence。进入知识库前
### 第一版范围
第一版只实现一个 `docs/ack/knowledge.yaml`,不拆目录。活跃条目达到几十条、单文件
开始影响审阅和选择时,再平滑迁移为 `docs/ack/knowledge/index.yaml` 加独立知识卡,
第一版只实现一个 `.pouch/ack/knowledge.yaml`,不拆目录。活跃条目达到几十条、单文件
开始影响审阅和选择时,再平滑迁移为 `.pouch/ack/knowledge/index.yaml` 加独立知识卡,
条目 schema 和引用格式保持不变。
第一版包括:
+1 -1
View File
@@ -16,7 +16,7 @@
## 项目配置契约
可选配置位于 `docs/ack/tasks.yaml` 的 `project.bugIntake`。未配置时 ACK 保持现有行为。
可选配置位于 `.pouch/ack/tasks.yaml` 的 `project.bugIntake`。未配置时 ACK 保持现有行为。
配置存在时必须包含:
| 字段 | 约束 |
+234
View File
@@ -0,0 +1,234 @@
# pouch 上手:安装、打版本、构建
pouch 是团队自研 skill 的仓库和安装工具。Skill 是给 Agent 看的工作说明书。你对着 Cursor、Claude Code 或 Codex 说话,Agent 按说明书干活。这份仓库里还有 CLI,用来把 skill 装到各 Agent。
这篇只讲日常会碰到的三件事:把 pouch 装上、用 releaser 打正式版本、用 builder 构建 DEB 或 Docker 镜像。其它 skill 装上就能喊,需要时对 Agent 说「有哪些自研 skill」。
## pouch 怎么工作
Skill 的正文只存在一份,在 `~/.pouch/skills/<name>/`。装到 Cursor、Claude、Codex 时走软链,改一份全处生效。项目根的 `.pouch.yaml` 记录这个仓库启用了哪些 skill,类似 `package.json` 记依赖。
日常几乎不直接跑 skill 里的脚本。CLI 负责安装和维护,干活是对 Agent 说人话。授权写在请求里:没说 push、没说上传,Agent 就不做。
| 你想做的 | 对谁说 | 不要 |
| ------------------- | -------------- | ----------------------- |
| 打正式版本、annotated tag | releaser | 让 builder 选下一个 SemVer |
| 打 DEB 或 Docker 镜像 | builder | 让 releaser 去构建或上传 |
releaser 选定正式 SemVer 并打 annotated tag。builder 只读当前 HEAD:HEAD 恰好落在 `vX.Y.Z` 上就打正式包,否则打测试包。
## 安装 pouch
需要 Python 3、git,以及至少一种兼容 Agent Skills 的 Agent。
```bash
git clone https://git.yumee.top/laily/pouch.git ~/.pouch
cd ~/.pouch && ./install.sh
export PATH="$HOME/.local/bin:$PATH"
pouch status
```
`install.sh` 会把 `pouch` 命令放到 `~/.local/bin`,并把 pouch 这一个 skill 全局装到所有 Agent。其它 skill 不会自动全装。当前 shell 找不到 `pouch` 时,重开终端,或 `source` 对应 rc。
也可以对 Agent 说「帮我安装 pouch」,或把仓库地址丢给它。不要覆盖已经存在的 `~/.pouch`。
从旧目录迁过来:
```bash
mv ~/.skills ~/.pouch
~/.pouch/install.sh
```
装完看一眼:
```bash
pouch list
pouch status
pouch doctor
```
软链异常就 `pouch doctor --fix`。更新 pouch 自身:`pouch update`(在 `~/.pouch` 里 `git pull`)。
## 把要用的 skill 装上
发版本和构建这两个,全局装一次,所有项目都能喊:
```bash
pouch add releaser -g -y
pouch add builder -g -y
```
只想在当前仓库启用:
```bash
cd ~/code/my-app
pouch add releaser -y
pouch add builder -y
```
也可以对 Agent 说「把这个项目该用的 skill 接上」,它会按仓库信号建议,再 `pouch add`。builder 不要 `pouch init builder`,初始化是对 Agent 说「用 builder 初始化这个项目」。
交互多选:`pouch select -g`。取消勾选不会卸载,卸载用 `pouch remove`。
## 怎么对 Agent 说话
打开已安装目标的 Agent。项目级安装要打开那个项目再说话;全局安装任意项目都可以。请求里写清 skill 名、目标和授权范围。
```text
使用 releaser,为已经合并到 main 的 1.6.0 发布 v1.6.0 tag。不要创建 Forge Release。
用 builder 初始化这个项目。
用 builder 构建当前版本的 DEB,先不要上传。
```
人看各 skill 目录下的 `README.md`。`SKILL.md` 是给 Agent 的指令,不必通读。
## 用 releaser 打新版本
代码已经合进默认分支、测试过了,要打正式 tag 时用它。它不构建 DEB,也不推镜像。
### 还没决定版本号
```text
使用 releaser 检查当前版本和这次改动,建议下一个版本号。只分析,不修改文件。
```
Agent 会读项目版本规则、现有 tag 和实际改动,给出建议和依据。没有项目约定时,单一协调版本默认 SemVer,tag 形如 `v1.6.0`。用户输入 `v1.6.0` 时规范版本是 `1.6.0`,不要产生双前缀。
### 默认短路径:直接打 tag
项目以 Git tag 作为版本来源,且不需要改 VERSION 文件或把 CHANGELOG 写回仓库时,走这条:
```text
使用 releaser,为已经合并到 main 的 1.6.0 发布 v1.6.0 tag。根据上一个稳定 tag 到目标 commit 的实际变化生成更新说明,把说明写入 annotated tag,并验证远端 tag 的 commit 和说明。不要创建 Forge Release。
```
请求里写清目标版本、基线分支、打到哪一步。没写 push、没写创建平台 Release,Agent 就不会做。只发 Git tag 时不要求 GitHub / GitLab / Gitea CLI,但 Git remote 必须可读写。
Agent 会锁定远端目标 commit,生成带 Summary、Changes、Validation、Base commit 的更新说明,创建 annotated tag 并推送,再从远端核对。不会给未合并分支打正式 tag,也不会覆盖已有同名 tag。
### 需要改版本文件时走长路径
权威版本文件或仓库内 CHANGELOG 必须随发布改掉时,才创建 `release/v1.6.0` 这类分支和 PR。可以一次说完,也可以拆开授权:
```text
使用 releaser 完成 1.6.0 发布:从 main 创建分支,完成版本更新和验证,创建 PR,检查通过后合并,推送 v1.6.0 tag。不要创建 Forge Release。
```
中途停了,下次说「使用 releaser 检查 v1.6.0 当前发布状态,从尚未完成的步骤继续。不要重复创建分支、PR、tag 或 release。」
要在 Gitea / GitHub 上建平台 Release 时,在请求里写明。没写就不会建。Forge Release 复用 tag 里同一份说明。
### 判断打没打成
远端 tag 解引用后的 commit 就是目标 commit,tag object 里有完整更新说明。只分析时会返回建议版本和依据。卡在某步时会说明停在哪、下次怎么续,不会用「发布成功」盖住未完成的步骤。
## 用 builder 构建项目
builder 按统一契约打 DEB 和 Docker 镜像。项目根会多两个文件:`makefile.builder`(构建入口)和 `.env.builder`(发布凭据,不进 git)。它不改你已有的 `Makefile` 或 `.env`。
正式版本号仍由 releaser 打 tag。builder 只读当前 HEAD,不调用 releaser。
### 接入一个项目
需要 Python 3 和 `make`。打 DEB 还要 `dpkg-deb`,打镜像还要 Docker 和 buildx。在项目根对 Agent 说:
```text
用 builder 初始化这个项目。
```
Agent 会探测轨道、写出 `makefile.builder`,缺发布配置时列出来。然后在 `.env.builder` 填仓库地址和 token:
```text
DEB_SERVER_URL=https://deb.example.com
DEB_REPOSITORY=main
DEB_TOKEN=<token>
DOCKER_REGISTRY=registry.example.com
```
token 只放 `.env.builder` 或密钥系统,不要写进 `.env`,也不要进 git。检查契约:
```text
用 builder 检查这个项目的 makefile.builder 是否符合契约。
```
构建项通过就可以打本地包。缺发布键只挡住上传。
日常构建入口:
```bash
make -f makefile.builder help
make -f makefile.builder version
make -f makefile.builder build
make -f makefile.builder deb
```
### 版本从哪来
`make -f makefile.builder version` 输出一行规范版本,无 `v` 前缀。基线是 HEAD 祖先上最近的稳定 tag,稳定 tag 仅匹配 `v<major>.<minor>.<patch>`,不含 `-rc` 这类后缀。
HEAD 恰好是 `v1.6.0` 时,产物是正式的 `1.6.0`。其它 commit 打测试号:祖先最近稳定 tag 的 patch 机械 +1,再挂构建日、分支、距离、短 SHA。测试 DEB 用 `~`,于是 `1.6.0` &lt; `1.6.1~…` &lt; `1.6.1`。已装上一正式版的机器可以升到 bugfix 测试包;同号正式包仍盖住测试。
分支 `feat/login-v2`,相对 `v1.6.0` 第 7 个 commit,短 SHA `abc1234`,构建日 `20260908`:
```text
规范 / DEB 1.6.1~20260908.feat-login-v2.7+gabc1234
Docker registry/ns/foo:1.6.1-20260908.feat-login-v2.7.gabc1234
```
不要自己 `git tag | sort -V | tail -1` 取全仓库最大号,也不要把 `git describe --dirty` 的原始字符串写进包名。
### 构建和发布
只构建:
```text
用 builder 构建当前版本的 DEB 和镜像,先不要上传。
```
确认后再上传:
```text
用 builder 把当前 commit 的产物发布到项目已配置的仓库。
```
自己跑也可以:
```bash
make -f makefile.builder deb
make -f makefile.builder push-deb
```
Docker 只有点名镜像时 Agent 才走。脏工作树默认拒绝发布。可能覆盖同版本时会先问你。token 不会出现在输出、提交或回复里。
## 一条完整发布链
代码合进 `main` 且检查通过之后,按这个顺序:
1. 对 Agent:`使用 releaser,为已经合并到 main 的 1.6.0 发布 v1.6.0 tag。不要创建 Forge Release。`
2. 确认远端 `v1.6.0` 指向那次 commit。
3. checkout 到该 tag,或确保当前 HEAD 就是它。
4. 对 Agent:`用 builder 构建当前版本的 DEB,先不要上传。`
5. 核对 `make -f makefile.builder version` 输出 `1.6.0`,再授权上传。
先构建再打 tag,得到的是测试包,版本号会变成下一 patch 的 `~` 形式,不是正式号。
## 常见卡住的地方
- 当前 shell 找不到 `pouch`:`~/.local/bin` 还没进 PATH,重开终端或 `source` 对应 rc。
- 在 `~/.cursor/skills/` 里直接改文件:改的是软链目标,应该改 `~/.pouch/skills/`。Claude Code 有时会把软链替换成普通文件,用 `pouch doctor --fix` 重建。
- `pouch init builder` 这条路径不存在:对 Agent 说「用 builder 初始化」。
- 请求里没写清授权:Agent 会分析或只做到本地,不会擅自 push tag 或上传包。
- 工作区有未提交修改:builder 默认拒绝发布;releaser 不会 stash 或覆盖你的文件。
- 同名 tag 已在远端指向别的 commit:releaser 会停,不会 force push。
- HEAD 不在稳定 tag 上却想打正式包:先打 tag,再构建。
## 还想看什么
- 仓库总览:[README.md](../README.md)
- CLI 命令:[pouch/README.md](../pouch/README.md)
- releaser:[skills/releaser/README.md](../skills/releaser/README.md)
- builder:[skills/builder/README.md](../skills/builder/README.md)
- 设计原则和编写规范:[AGENTS.md](../AGENTS.md)
+10 -5
View File
@@ -3,17 +3,22 @@ set -euo pipefail
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BIN_DIR="${HOME}/.local/bin"
PATH_MARKER="# skiff: ~/.local/bin"
PATH_MARKER="# pouch: ~/.local/bin"
LEGACY_PATH_MARKER="# skiff: ~/.local/bin"
mkdir -p "$BIN_DIR"
ln -sf "$REPO/bin/skiff" "$BIN_DIR/skiff"
echo "已安装 skiff -> $BIN_DIR/skiff"
ln -sf "$REPO/bin/pouch" "$BIN_DIR/pouch"
echo "已安装 pouch -> $BIN_DIR/pouch"
if [[ ! -e "$BIN_DIR/skiff" || -L "$BIN_DIR/skiff" ]]; then
ln -sf "pouch" "$BIN_DIR/skiff"
echo "已保留 skiff -> pouch(旧命令名)"
fi
PYTHONPATH="$REPO${PYTHONPATH:+:$PYTHONPATH}" python3 -m skiff bootstrap
PYTHONPATH="$REPO${PYTHONPATH:+:$PYTHONPATH}" python3 -m pouch bootstrap
path_already_configured() {
local file="$1"
[[ -f "$file" ]] && grep -qF "$PATH_MARKER" "$file"
[[ -f "$file" ]] && { grep -qF "$PATH_MARKER" "$file" || grep -qF "$LEGACY_PATH_MARKER" "$file"; }
}
configure_bash() {
-20
View File
@@ -1,20 +0,0 @@
schema: spec-driven
# Project context (optional)
# This is shown to AI when creating artifacts.
# Add your tech stack, conventions, style guides, domain knowledge, etc.
# Example:
# context: |
# Tech stack: TypeScript, React, Node.js
# We use conventional commits
# Domain: e-commerce platform
# Per-artifact rules (optional)
# Add custom rules for specific artifacts.
# Example:
# rules:
# proposal:
# - Keep proposals under 500 words
# - Always include a "Non-goals" section
# tasks:
# - Break tasks into chunks of max 2 hours
+254
View File
@@ -0,0 +1,254 @@
# pouch
Agent Skills 安装与管理 CLI。纯 Python 3 实现,无第三方依赖,无需编译。
## 安装
```bash
cd /path/to/pouch # 本仓库根目录
./install.sh # 软链到 ~/.local/bin/pouch
```
确保 `~/.local/bin` 在 `PATH` 中。
## 命令风格
接口对齐 [Vercel skills CLI](https://github.com/vercel-labs/skills) 的 `add` / `remove`,
统一管理 builtin skill、预置 catalog source 和用户命名的 custom source。
```bash
# 浏览可用自研 skill
pouch add --list
# 装到当前项目 / 全局
pouch add discussion-notes -a cursor -y
pouch add discussion-notes -a cursor -g -y
# 卸载
pouch remove discussion-notes -a cursor -y
pouch rm discussion-notes -g -y
# 改完 skill 后提交推送(在任意目录执行,操作 ~/.pouch)
pouch publish skills/discussion-notes -m "update discussion-notes" --push
```
开发时也可直接运行:
```bash
PYTHONPATH=/path/to/pouch python3 -m pouch <command>
```
## 首次安装
```bash
git clone https://git.yumee.top/laily/pouch.git ~/.pouch
~/.pouch/install.sh
```
`install.sh` 会安装 CLI,并自动执行 `pouch bootstrap`,将本仓库的 `pouch` skill 全局软链到 Cursor、Claude Code 和 Codex。也可以随时手动重跑:
```bash
pouch bootstrap
```
## 命令参考
### 查看
| 命令 | 说明 |
|------|------|
| `pouch list [--source NAME]` | 列出所有来源或指定 source 中的 skill |
| `pouch status [--target all\|cursor\|claude\|codex\|agents]` | 安装状态总览 |
### 项目初始化
| 命令 | 说明 |
|------|------|
| `pouch bootstrap` | 将本项目的 `pouch` skill 全局安装到所有 Agent |
| `pouch update` | 在 `~/.pouch` 执行 `git pull`,更新 pouch 自身 |
| `pouch init <name> [--project DIR]` | 使用 builtin skill 自带模板初始化项目状态(目前用于 ack) |
### Skill 安装
| 命令 | 说明 |
|------|------|
| `pouch add <name> [--global] [-a AGENT...] [-y]` | 安装到 Agent 目录(软链) |
| `pouch select [--global] [-a AGENT...]` | 打开终端多选界面,批量安装 skill |
| `pouch remove <name> [--global] [-a AGENT...] [-y]` | 移除软链(`rm` / `r` 别名) |
| `pouch add --list` | 列出可用 builtin skill |
| `pouch publish [paths] -m MSG [--push]` | 在 ~/.pouch 内 git add/commit/push |
旧命令 `install` / `uninstall` 已移除,请改用 `add` / `remove`。
全局目标路径:
| Agent | 路径 |
|-------|------|
| cursor | `~/.cursor/skills/` |
| claude | `~/.claude/skills/` |
| codex | `~/.codex/skills/` |
| agents | `~/.agents/skills/`(agents 标准目录,覆盖 OMP) |
### 预置 Catalog Source
| 命令 | 说明 |
|------|------|
| `pouch catalog add <name> <repo-url> [--ref main] [--path .]` | 写入 `catalog.yaml` |
| `pouch fetch <name>` | 克隆或更新 catalog source checkout |
| `pouch add <name> [-g] [-a AGENT...]` | 安装 catalog 中的单个 skill 或完整 collection(缺失时自动 fetch) |
| `pouch add <collection>/<skill> [...]` | 只安装 collection 中指定的 skill |
`catalog.yaml` 条目可额外提供 `description` 和 `tags`。`description`
会显示在 `pouch select` 的候选列表中。`path` 可以直接指向含
`SKILL.md` 的单个 skill,也可以指向由多个 skill 目录组成的 collection。
collection 会自动发现下一层所有含 `SKILL.md` 的目录;`pouch add <name>`
安装全部,`pouch select` 则展开为 `<name>/<skill>` 供分别勾选。同一
`repo` 与 `ref` 共享一份 Git checkout。
### 交互式批量安装
```bash
pouch select # 当前项目,全部 Agent
pouch select -a codex # 当前项目,仅 Codex
pouch select -g # 全局安装
pouch select --project ~/code/app # 指定项目
```
使用方向键移动、空格勾选、`/` 搜索、Enter 安装,按 `q` 或 Esc
取消。普通 `pouch select` 只安装到项目,并在每一项旁只读显示各 Agent 的
全局安装状态;`pouch select -g` 只安装到全局。已经安装到目标范围的 skill
默认勾选;取消勾选不会卸载已有 skill,卸载请使用 `pouch remove`。如果全局
存在同名但指向其它来源的 skill,项目选择器会显示“全局同名冲突”。
项目模式会把成功选择的项目写入 `.pouch.yaml`。非交互环境请使用
`pouch add <name>...`。使用 `-a` 限定 Agent 时,该范围会记录在对应的
skill 条目中,后续 `pouch sync` 不会扩散到其他 Agent。
### Custom source(多-skill 仓库)
公司或团队维护的仓库通常包含多个 skill。使用命名 source 接入:
```bash
# Git 仓库,默认 clone 到 ~/.local/share/pouch/sources/company
pouch source add company \
git@git.company.com:platform/agent-skills.git \
--ref main \
--skills-path internal/skills
# 或接入已有本地仓库
pouch source add company \
--local ~/code/company-agent-skills \
--skills-path skills
pouch source list
pouch source fetch company
pouch list --source company
pouch add company/code-review -g -a codex
```
| 命令 | 说明 |
|------|------|
| `pouch source add <name> <repo> [--ref REF] [--checkout PATH] [--skills-path PATH]` | 注册并克隆 Git source |
| `pouch source add <name> --local PATH [--skills-path PATH]` | 接入已有本地仓库 |
| `pouch source list` / `show <name>` | 查看 source |
| `pouch source fetch <name>` / `fetch --all` | clone 或 fast-forward 更新 |
| `pouch source remove <name>` | 移除配置并保留 checkout |
配置保存在 `~/.config/pouch/config.yaml`。Git/SSH 认证复用本机 Git 配置,
pouch 不保存 token。可以使用 `company/code-review`,也可以使用
`pouch add code-review --source company`。多个来源包含同名 skill 时,必须明确来源。
### 项目级
| 命令 | 说明 |
|------|------|
| `pouch enable <name> [--target all] [--project <dir>]` | 写入 `.pouch.yaml` 并创建项目软链 |
| `pouch disable <name> [--target all] [--project <dir>]` | 从 manifest 移除并删除软链 |
| `pouch sync [--target all] [--project <dir>]` | 按 `.pouch.yaml` 重建软链 |
项目目标路径:
| Agent | 路径 |
|-------|------|
| cursor | `<project>/.agents/skills/` |
| claude | `<project>/.claude/skills/` |
| codex | `<project>/.agents/skills/` |
| agents | `<project>/.agents/skills/`(与 cursor/codex 共用路径,软链幂等) |
### 脚手架与健康检查
| 命令 | 说明 |
|------|------|
| `pouch create <name> [--idea TEXT] [--from-project PATH]` | 从模板创建含 `SKILL.md`、`README.md` 的草稿 |
| `pouch check <name>` | 校验草稿或正式 skill,包括人类使用说明 |
| `pouch finalize <name>` | 校验草稿并移动到正式 `skills/` |
| `pouch doctor [--target all] [--fix]` | 检查软链健康状态,`--fix` 自动修复 |
## 常用工作流
### 新建并全局启用自研 skill
```bash
pouch create my-skill --idea "描述要解决的重复问题" --from-project .
# 由 Agent 完善草稿中的 SKILL.md 和 README.md
pouch check my-skill
pouch finalize my-skill
pouch publish skills/my-skill -m "add my-skill" --push
pouch add my-skill -a cursor -g -y
pouch doctor -a cursor
```
### 在项目中启用 skill
```bash
cd ~/code/my-app
pouch add discussion-notes -a cursor -y
```
### 添加 Catalog Source
```bash
pouch catalog add my-ext https://github.com/org/repo --ref main
pouch fetch my-ext
pouch add my-ext -g
```
## 源码结构
```
pouch/
├── __init__.py # 版本号
├── __main__.py # python3 -m pouch 入口
├── cli.py # 命令定义与调度
├── paths.py # 路径常量与 Agent 目标
├── skills.py # builtin/catalog/custom 统一解析
├── catalog.py # catalog.yaml 读写与 Skill 发现
├── sources.py # custom source 配置、发现与 Git 管理
├── project.py # .pouch.yaml 管理
├── symlinks.py # 软链创建/检查/修复
└── yaml_io.py # 轻量 YAML 解析(无第三方依赖)
```
入口脚本:[../bin/pouch](../bin/pouch)
## 路径约定
| 变量 | 路径 | 说明 |
|------|------|------|
| `POUCH_HOME` | `~/.pouch` | pouch 仓库(软链;兼容 `~/.skills`) |
| `SKILLS_DIR` | `~/.pouch/skills/` | builtin skill 目录 |
| `CATALOG_FILE` | `~/.pouch/catalog.yaml` | 预置 Skill 来源目录 |
| `CATALOG_CACHE_DIR` | `~/.local/share/pouch/externals/` | catalog checkout 缓存;按 repo/ref 共享 |
| `CONFIG_FILE` | `~/.config/pouch/config.yaml` | custom source 配置 |
| `SOURCES_DIR` | `~/.local/share/pouch/sources/` | custom Git source 默认 checkout |
## 注意事项
- **禁止**在 `~/.cursor/skills/` 等 Agent 目录直接创建非软链的 skill
- Claude Code 对 symlink 支持不稳定;建议对单个 skill 目录软链,不要软链整个 `~/.claude/skills/`
- 若 Agent 将软链替换为普通目录,运行 `pouch doctor --fix` 重建
## 相关文档
- [项目 README](../README.md)
- [AGENTS.md](../AGENTS.md)
- 各 skill 的人类说明:`skills/<name>/README.md`
+3
View File
@@ -0,0 +1,3 @@
"""pouch — Agent Skills 安装与管理 CLI。"""
__version__ = "0.6.0"
+1 -1
View File
@@ -1,4 +1,4 @@
from skiff.cli import main
from pouch.cli import main
if __name__ == "__main__":
main()
+1 -1
View File
@@ -2,7 +2,7 @@
from __future__ import annotations
from skiff.paths import ALL_TARGETS
from pouch.paths import ALL_TARGETS
AGENT_ALIASES: dict[str, str] = {
"cursor": "cursor",
+4 -4
View File
@@ -6,8 +6,8 @@ import hashlib
from pathlib import Path
from typing import Any
from skiff import yaml_io
from skiff.paths import CATALOG_FILE, LEGACY_REGISTRY_FILE
from pouch import yaml_io
from pouch.paths import CATALOG_FILE, LEGACY_REGISTRY_FILE
def load_catalog(path: Path | None = None) -> dict[str, dict[str, Any]]:
@@ -36,7 +36,7 @@ def catalog_repo(entry: dict[str, Any]) -> str:
def catalog_repo_path(entry: dict[str, Any]) -> Path:
"""Return the shared checkout path for a repo/ref pair."""
from skiff.paths import CATALOG_CACHE_DIR
from pouch.paths import CATALOG_CACHE_DIR
repo = str(entry.get("repo", ""))
ref = str(entry.get("ref", "main"))
@@ -46,7 +46,7 @@ def catalog_repo_path(entry: dict[str, Any]) -> Path:
def catalog_checkout_path(name: str, entry: dict[str, Any]) -> Path:
"""Use a local repo directly, otherwise return its external checkout."""
from skiff.paths import CATALOG_CACHE_DIR
from pouch.paths import CATALOG_CACHE_DIR
configured_repo = str(entry.get("repo", ""))
local_repo = Path(catalog_repo(entry))
+97 -96
View File
@@ -1,4 +1,4 @@
"""skiff CLI 入口。"""
"""pouch CLI 入口。"""
from __future__ import annotations
@@ -15,28 +15,29 @@ import tempfile
from datetime import datetime
from pathlib import Path
from skiff import __version__
from skiff.agents import flatten_agent_args, resolve_agent_args
from skiff.gitops import publish as git_publish
from skiff.paths import (
from pouch import __version__
from pouch.agents import flatten_agent_args, resolve_agent_args
from pouch.gitops import publish as git_publish
from pouch.paths import (
ALL_TARGETS,
DRAFTS_DIR,
CATALOG_CACHE_DIR,
CONFIG_FILE,
SKILLS_DIR,
SKILLS_HOME,
POUCH_HOME,
TEMPLATE_DIR,
agent_skill_dir,
ensure_skills_home,
ensure_pouch_home,
project_manifest,
)
from skiff.project import (
from pouch.project import (
add_skill_to_manifest,
iter_manifest_skills,
load_manifest,
remove_skill_from_manifest,
resolve_manifest_skill,
)
from skiff.catalog import (
from pouch.catalog import (
discover_catalog_skills,
catalog_checkout_path,
catalog_skill_path,
@@ -44,8 +45,8 @@ from skiff.catalog import (
catalog_repo,
save_catalog,
)
from skiff.selector import SkillChoice, select_skills
from skiff.skills import (
from pouch.selector import SkillChoice, select_skills
from pouch.skills import (
list_custom_skills,
list_builtin_skills,
builtin_skill_path,
@@ -57,7 +58,7 @@ from skiff.skills import (
validate_skill_dir,
validate_skill_name,
)
from skiff.sources import (
from pouch.sources import (
discover_source_skills,
fetch_source,
load_sources,
@@ -66,8 +67,8 @@ from skiff.sources import (
source_skills_root,
validate_source_name,
)
from skiff.yaml_io import safe_dump
from skiff.symlinks import check_link, copy_template, create_link, find_repo_root, remove_link
from pouch.yaml_io import safe_dump
from pouch.symlinks import check_link, copy_template, create_link, find_repo_root, remove_link
_RENAME_NOREPLACE = 1
@@ -485,7 +486,7 @@ def _remove_skill(
def cmd_list(args: argparse.Namespace) -> None:
ensure_skills_home()
ensure_pouch_home()
source_filter = normalize_source(args.source)
builtin = list_builtin_skills()
catalog = load_catalog()
@@ -528,18 +529,18 @@ def cmd_list(args: argparse.Namespace) -> None:
def cmd_bootstrap(args: argparse.Namespace) -> None:
del args
ensure_skills_home()
project_skill = "skiff"
ensure_pouch_home()
project_skill = "pouch"
builtin_skill_path(project_skill)
_install_skill(project_skill, list(ALL_TARGETS), project_root=None)
_print("已全局安装 builtin skiff skill 到所有 agent")
_print("已全局安装 builtin pouch skill 到所有 agent")
def cmd_update(args: argparse.Namespace) -> None:
del args
ensure_skills_home()
_print(f"更新 skiff: {SKILLS_HOME}")
subprocess.run(["git", "-C", str(SKILLS_HOME), "pull"], check=True)
ensure_pouch_home()
_print(f"更新 pouch: {POUCH_HOME}")
subprocess.run(["git", "-C", str(POUCH_HOME), "pull"], check=True)
def _installed_links(
@@ -558,7 +559,7 @@ def _installed_links(
def cmd_status(args: argparse.Namespace) -> None:
ensure_skills_home()
ensure_pouch_home()
targets = resolve_agent_args(flatten_agent_args(args.agents))
builtin = list_builtin_skills()
catalog = load_catalog()
@@ -578,7 +579,7 @@ def cmd_status(args: argparse.Namespace) -> None:
)
entries.extend((source, name) for source, names in custom.items() for name in names)
_print(f"skills 仓库: {SKILLS_HOME.resolve()}")
_print(f"pouch 仓库: {POUCH_HOME.resolve()}")
_print(f"agents: {', '.join(targets)}\n")
for package in unfetched_catalog:
@@ -604,10 +605,10 @@ def cmd_status(args: argparse.Namespace) -> None:
def _print_available_skills() -> None:
ensure_skills_home()
ensure_pouch_home()
builtin = list_builtin_skills()
if not builtin:
_print("~/.skills/skills/ 中没有自研 skill")
_print("~/.pouch/skills/ 中没有自研 skill")
return
_print(f"来源: {SKILLS_DIR}\n")
@@ -621,14 +622,14 @@ def _print_available_skills() -> None:
def cmd_add(args: argparse.Namespace) -> None:
ensure_skills_home()
ensure_pouch_home()
if args.list_available:
if args.source:
cmd_list(argparse.Namespace(source=args.source))
else:
_print_available_skills()
_print("使用 skiff add <name> 安装,或 skiff add <name> -g 全局安装")
_print("使用 pouch add <name> 安装,或 pouch add <name> -g 全局安装")
return
if args.all:
@@ -647,7 +648,7 @@ def cmd_add(args: argparse.Namespace) -> None:
else:
names = _collect_skill_names(args.skills, args.skills_flag)
if not names:
raise SystemExit("请指定 skill 名称,或使用 skiff add --list 查看可用 skill")
raise SystemExit("请指定 skill 名称,或使用 pouch add --list 查看可用 skill")
targets = resolve_agent_args(flatten_agent_args(args.agents))
project_root = None if args.global_scope else _project_root(args.project)
@@ -671,7 +672,7 @@ def cmd_add(args: argparse.Namespace) -> None:
(link, str(link.readlink()) if link.is_symlink() else None)
)
manifest_path = project_root / ".skills.yaml" if project_root else None
manifest_path = project_manifest(project_root) if project_root else None
manifest_before = (
manifest_path.read_bytes()
if manifest_path and manifest_path.is_file()
@@ -709,10 +710,10 @@ def cmd_add(args: argparse.Namespace) -> None:
def cmd_select(args: argparse.Namespace) -> None:
ensure_skills_home()
ensure_pouch_home()
if not sys.stdin.isatty() or not sys.stdout.isatty():
raise SystemExit(
"`skiff select` 需要交互式终端;非交互环境请使用 `skiff add <name>...`"
"`pouch select` 需要交互式终端;非交互环境请使用 `pouch add <name>...`"
)
targets = resolve_agent_args(flatten_agent_args(args.agents))
@@ -877,7 +878,7 @@ def cmd_select(args: argparse.Namespace) -> None:
names = sorted(selected - selected_installed_keys)
failures: list[tuple[str, str]] = []
manifest_path = project_root / ".skills.yaml" if project_root else None
manifest_path = project_manifest(project_root) if project_root else None
successful = set(selected & selected_installed_keys)
for name in names:
skill_name, source = choice_requests[name]
@@ -915,7 +916,7 @@ def cmd_select(args: argparse.Namespace) -> None:
def cmd_remove(args: argparse.Namespace) -> None:
ensure_skills_home()
ensure_pouch_home()
project_root = None if args.global_scope else _project_root(args.project)
targets = resolve_agent_args(flatten_agent_args(args.agents))
@@ -925,7 +926,7 @@ def cmd_remove(args: argparse.Namespace) -> None:
names = _collect_skill_names(args.skills, args.skills_flag)
if not names:
raise SystemExit("请指定 skill 名称,或使用 skiff remove --all")
raise SystemExit("请指定 skill 名称,或使用 pouch remove --all")
catalog = load_catalog()
sources = load_sources()
@@ -958,14 +959,14 @@ def cmd_remove(args: argparse.Namespace) -> None:
for name in dict.fromkeys(expanded):
total += _remove_skill(name, targets, project_root=project_root)
if project_root is not None:
remove_skill_from_manifest(project_root / ".skills.yaml", name)
remove_skill_from_manifest(project_manifest(project_root), name)
if total == 0:
_print("没有移除任何 skill")
def cmd_publish(args: argparse.Namespace) -> None:
ensure_skills_home()
ensure_pouch_home()
paths = args.paths or ["."]
git_publish(
paths=paths,
@@ -976,7 +977,7 @@ def cmd_publish(args: argparse.Namespace) -> None:
def cmd_catalog_add(args: argparse.Namespace) -> None:
ensure_skills_home()
ensure_pouch_home()
validate_skill_name(args.name)
catalog = load_catalog()
if args.name in catalog:
@@ -992,7 +993,7 @@ def cmd_catalog_add(args: argparse.Namespace) -> None:
def cmd_fetch(args: argparse.Namespace) -> None:
ensure_skills_home()
ensure_pouch_home()
catalog = load_catalog()
if args.name not in catalog:
raise SystemExit(f"catalog 中不存在: {args.name}")
@@ -1106,9 +1107,9 @@ def cmd_source_remove(args: argparse.Namespace) -> None:
def cmd_sync(args: argparse.Namespace) -> None:
ensure_skills_home()
ensure_pouch_home()
root = _project_root(args.project)
manifest_path = root / ".skills.yaml"
manifest_path = project_manifest(root)
if not manifest_path.is_file():
raise SystemExit(f"未找到 {manifest_path}")
@@ -1133,7 +1134,7 @@ def cmd_sync(args: argparse.Namespace) -> None:
def cmd_create(args: argparse.Namespace) -> None:
ensure_skills_home()
ensure_pouch_home()
validate_skill_name(args.name)
if not TEMPLATE_DIR.is_dir():
raise SystemExit(f"模板目录不存在: {TEMPLATE_DIR}")
@@ -1170,8 +1171,8 @@ def cmd_create(args: argparse.Namespace) -> None:
}
(dst / "brief.yaml").write_text(safe_dump(brief), encoding="utf-8")
_print(f"草稿已创建: {dst}")
_print(f"下一步: 请完善 skiff 草稿 {args.name}")
_print(f"完成后运行: skiff check {args.name} && skiff finalize {args.name}")
_print(f"下一步: 请完善 pouch 草稿 {args.name}")
_print(f"完成后运行: pouch check {args.name} && pouch finalize {args.name}")
def _draft_or_builtin_path(name: str) -> tuple[Path, str]:
@@ -1185,7 +1186,7 @@ def _draft_or_builtin_path(name: str) -> tuple[Path, str]:
def cmd_check(args: argparse.Namespace) -> None:
ensure_skills_home()
ensure_pouch_home()
validate_skill_name(args.name)
path, kind = _draft_or_builtin_path(args.name)
issues = validate_skill_dir(path, args.name)
@@ -1199,7 +1200,7 @@ def cmd_check(args: argparse.Namespace) -> None:
def cmd_finalize(args: argparse.Namespace) -> None:
ensure_skills_home()
ensure_pouch_home()
validate_skill_name(args.name)
draft = DRAFTS_DIR / args.name
if not draft.is_dir():
@@ -1221,25 +1222,25 @@ def cmd_finalize(args: argparse.Namespace) -> None:
if brief.exists():
brief.unlink()
_print(f"已完成 skill: {final}")
_print(f"下一步: skiff publish skills/{args.name} -m \"add {args.name}\" --push")
_print(f"下一步: pouch publish skills/{args.name} -m \"add {args.name}\" --push")
def cmd_doctor(args: argparse.Namespace) -> None:
ensure_skills_home()
ensure_pouch_home()
targets = resolve_agent_args(flatten_agent_args(args.agents))
issues = 0
_print(f"skills 仓库: {SKILLS_HOME.resolve()}")
if SKILLS_HOME.is_symlink():
if not SKILLS_HOME.resolve().is_dir():
_err(f"✗ ~/.skills 指向无效路径: {SKILLS_HOME.resolve()}")
_print(f"pouch 仓库: {POUCH_HOME.resolve()}")
if POUCH_HOME.is_symlink():
if not POUCH_HOME.resolve().is_dir():
_err(f"✗ ~/.pouch 指向无效路径: {POUCH_HOME.resolve()}")
issues += 1
else:
_print("✓ ~/.skills 软链正常")
elif SKILLS_HOME.is_dir() and (SKILLS_HOME / "skills").is_dir():
_print("✓ ~/.skills 为本地仓库目录")
_print("✓ ~/.pouch 软链正常")
elif POUCH_HOME.is_dir() and (POUCH_HOME / "skills").is_dir():
_print("✓ ~/.pouch 为本地仓库目录")
else:
_err("✗ ~/.skills 未正确配置")
_err("✗ ~/.pouch 未正确配置")
issues += 1
for name in list_builtin_skills():
@@ -1278,13 +1279,13 @@ def cmd_doctor(args: argparse.Namespace) -> None:
else:
_print(f"\n发现 {issues} 个问题")
if not args.fix:
_print("提示: 使用 skiff doctor --fix 尝试自动修复软链")
_print("提示: 使用 pouch doctor --fix 尝试自动修复软链")
sys.exit(1)
def cmd_init(args: argparse.Namespace) -> None:
"""使用 builtin skill 自带的模板初始化目标项目状态。"""
ensure_skills_home()
ensure_pouch_home()
validate_skill_name(args.name)
skills_root = SKILLS_DIR.resolve()
skill_source = (SKILLS_DIR / args.name).resolve()
@@ -1307,7 +1308,7 @@ def cmd_init(args: argparse.Namespace) -> None:
initial_project_stat.st_ino,
stat.S_IFMT(initial_project_stat.st_mode),
)
destination = project / "docs" / args.name
destination = project / ".pouch" / args.name
project_file = destination / "project.md"
tasks_file = destination / "tasks.yaml"
knowledge_file = destination / "knowledge.yaml"
@@ -1340,7 +1341,7 @@ def cmd_init(args: argparse.Namespace) -> None:
)
missing = [path for path, _ in template_targets if not path.is_file()]
if missing:
paths = ", ".join(str(path.relative_to(SKILLS_HOME)) for path in missing)
paths = ", ".join(str(path.relative_to(POUCH_HOME)) for path in missing)
raise SystemExit(f"skill 缺少初始化模板: {paths}")
validator = skill_source / "scripts" / "validate_tasks.py"
knowledge_validator = skill_source / "scripts" / "validate_knowledge.py"
@@ -1368,13 +1369,13 @@ def cmd_init(args: argparse.Namespace) -> None:
"<project_name>": project.name,
"<repo_path>": str(project),
"<dev_worktree>": str(project),
"<overlay_file_path>": f"docs/{args.name}/project.md",
"<overlay_file_path>": f".pouch/{args.name}/project.md",
"<ack_version>": ack_version,
"<接入时的 ack skill 版本>": ack_version,
"<YYYY-MM-DDTHH:mm:ss+TZ>": now,
}
with tempfile.TemporaryDirectory(prefix=f"skiff-{args.name}-init-") as temp_dir:
with tempfile.TemporaryDirectory(prefix=f"pouch-{args.name}-init-") as temp_dir:
staging = Path(temp_dir)
staged_files: dict[Path, Path] = {}
rendered_files: dict[Path, str] = {}
@@ -1456,10 +1457,10 @@ def cmd_init(args: argparse.Namespace) -> None:
raise SystemExit(f"拒绝覆盖已有路径: {paths}")
project_fd: int | None = None
docs_fd: int | None = None
pouch_fd: int | None = None
transaction_fd: int | None = None
staging_fd: int | None = None
docs_created = False
pouch_created = False
transaction_name: str | None = None
staged_names: list[str] = []
published = False
@@ -1487,8 +1488,8 @@ def cmd_init(args: argparse.Namespace) -> None:
project,
phase="初始化",
)
docs_fd, docs_created = _open_or_create_directory_at(project_fd, "docs")
if docs_created:
pouch_fd, pouch_created = _open_or_create_directory_at(project_fd, ".pouch")
if pouch_created:
os.fsync(project_fd)
_assert_open_directory_path(
project_fd,
@@ -1496,15 +1497,15 @@ def cmd_init(args: argparse.Namespace) -> None:
phase="初始化",
)
_assert_open_directory_path(
docs_fd,
project / "docs",
pouch_fd,
project / ".pouch",
phase="初始化",
label="docs 目录",
label=".pouch 目录",
)
try:
destination_stat = os.stat(
args.name,
dir_fd=docs_fd,
dir_fd=pouch_fd,
follow_symlinks=False,
)
except FileNotFoundError:
@@ -1513,14 +1514,14 @@ def cmd_init(args: argparse.Namespace) -> None:
if stat.S_ISLNK(destination_stat.st_mode):
raise SystemExit(
"初始化路径必须是普通目录且不能是软链接: "
f"docs/{args.name}"
f".pouch/{args.name}"
)
raise SystemExit(f"拒绝覆盖已有路径: docs/{args.name}")
raise SystemExit(f"拒绝覆盖已有路径: .pouch/{args.name}")
for _ in range(32):
candidate = f".{args.name}-init-{secrets.token_hex(8)}"
try:
os.mkdir(candidate, mode=0o700, dir_fd=docs_fd)
os.mkdir(candidate, mode=0o700, dir_fd=pouch_fd)
except FileExistsError:
continue
transaction_name = candidate
@@ -1531,7 +1532,7 @@ def cmd_init(args: argparse.Namespace) -> None:
transaction_fd = os.open(
transaction_name,
os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW,
dir_fd=docs_fd,
dir_fd=pouch_fd,
)
os.mkdir("payload", mode=0o755, dir_fd=transaction_fd)
staging_fd = os.open(
@@ -1560,21 +1561,21 @@ def cmd_init(args: argparse.Namespace) -> None:
phase="发布",
)
_assert_open_directory_path(
docs_fd,
project / "docs",
pouch_fd,
project / ".pouch",
phase="发布",
label="docs 目录",
label=".pouch 目录",
)
try:
_rename_directory_noreplace(
transaction_fd,
"payload",
docs_fd,
pouch_fd,
args.name,
)
except FileExistsError as exc:
raise SystemExit(
f"拒绝覆盖已有路径: docs/{args.name}"
f"拒绝覆盖已有路径: .pouch/{args.name}"
) from exc
published = True
_assert_open_directory_path(
@@ -1586,23 +1587,23 @@ def cmd_init(args: argparse.Namespace) -> None:
if (
transaction_name is not None
and _directory_entry_matches_open_fd(
docs_fd,
pouch_fd,
transaction_name,
transaction_fd,
)
):
try:
os.rmdir(transaction_name, dir_fd=docs_fd)
os.rmdir(transaction_name, dir_fd=pouch_fd)
except OSError:
pass
else:
transaction_name = None
try:
os.fsync(docs_fd)
os.fsync(pouch_fd)
except OSError as exc:
raise SystemExit(
"初始化目录已完整发布,但无法确认目录项持久化;"
f"请检查 docs/{args.name} 后再重试"
f"请检查 .pouch/{args.name} 后再重试"
) from exc
_assert_open_directory_path(
project_fd,
@@ -1610,10 +1611,10 @@ def cmd_init(args: argparse.Namespace) -> None:
phase="完成初始化",
)
_assert_open_directory_path(
docs_fd,
project / "docs",
pouch_fd,
project / ".pouch",
phase="完成初始化",
label="docs 目录",
label=".pouch 目录",
)
_assert_open_directory_path(
staging_fd,
@@ -1638,15 +1639,15 @@ def cmd_init(args: argparse.Namespace) -> None:
if (
transaction_name is not None
and transaction_fd is not None
and docs_fd is not None
and pouch_fd is not None
and _directory_entry_matches_open_fd(
docs_fd,
pouch_fd,
transaction_name,
transaction_fd,
)
):
try:
os.rmdir(transaction_name, dir_fd=docs_fd)
os.rmdir(transaction_name, dir_fd=pouch_fd)
except OSError:
pass
raise
@@ -1654,7 +1655,7 @@ def cmd_init(args: argparse.Namespace) -> None:
for directory_fd in (
staging_fd,
transaction_fd,
docs_fd,
pouch_fd,
project_fd,
):
if directory_fd is not None:
@@ -1697,20 +1698,20 @@ def _add_common_flags(parser: argparse.ArgumentParser) -> None:
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="skiff",
prog="pouch",
description="自研 Agent Skills 安装与管理 CLI(接口对齐 Vercel skills)",
)
parser.add_argument("--version", action="version", version=f"skiff {__version__}")
parser.add_argument("--version", action="version", version=f"pouch {__version__}")
sub = parser.add_subparsers(dest="command", required=True)
p_bootstrap = sub.add_parser(
"bootstrap",
help="将本项目 skiff skill 全局安装到所有 agent",
help="将本项目 pouch skill 全局安装到所有 agent",
)
p_bootstrap.set_defaults(func=cmd_bootstrap)
p_update = sub.add_parser("update", help="通过 git pull 更新 skiff 自身")
p_update = sub.add_parser("update", help="通过 git pull 更新 pouch 自身")
p_update.set_defaults(func=cmd_update)
p_list = sub.add_parser("list", help="列出所有 source 中的 skill")
@@ -1754,7 +1755,7 @@ def build_parser() -> argparse.ArgumentParser:
p_publish = sub.add_parser(
"publish",
help="在 ~/.skills 内 git add / commit / push",
help="在 ~/.pouch 内 git add / commit / push",
)
p_publish.add_argument("paths", nargs="*", help="要提交的路径(默认 .)")
p_publish.add_argument("-m", "--message", help="commit 说明")
@@ -1805,11 +1806,11 @@ def build_parser() -> argparse.ArgumentParser:
p_source_remove.add_argument(
"--delete-checkout",
action="store_true",
help="同时永久删除 skiff 管理的 checkout",
help="同时永久删除 pouch 管理的 checkout",
)
p_source_remove.set_defaults(func=cmd_source_remove)
p_sync = sub.add_parser("sync", help="按 .skills.yaml 重建项目软链")
p_sync = sub.add_parser("sync", help="按 .pouch.yaml 重建项目软链")
p_sync.add_argument("-a", "--agent", dest="agents", nargs="+", action="append")
p_sync.add_argument("--project")
p_sync.set_defaults(func=cmd_sync)
+3 -3
View File
@@ -1,4 +1,4 @@
"""~/.skills 仓库内的 git 操作。"""
"""~/.pouch 仓库内的 git 操作。"""
from __future__ import annotations
@@ -6,7 +6,7 @@ import subprocess
import sys
from pathlib import Path
from skiff.paths import SKILLS_HOME
from pouch.paths import POUCH_HOME
def _run_git(repo: Path, *args: str, check: bool = True) -> subprocess.CompletedProcess[str]:
@@ -35,7 +35,7 @@ def publish(
push: bool,
no_commit: bool,
) -> None:
repo = SKILLS_HOME.resolve()
repo = POUCH_HOME.resolve()
ensure_git_repo(repo)
_run_git(repo, "add", "--", *paths)
+103
View File
@@ -0,0 +1,103 @@
"""路径与 Agent 目标定义。"""
from __future__ import annotations
from pathlib import Path
HOME = Path.home()
PREFERRED_POUCH_HOME = HOME / ".pouch"
LEGACY_POUCH_HOME = HOME / ".skills"
PROJECT_MANIFEST = ".pouch.yaml"
LEGACY_PROJECT_MANIFEST = ".skills.yaml"
def _first_existing_dir(*candidates: Path, default: Path) -> Path:
for path in candidates:
if path.is_dir():
return path
return default
def _first_existing_file(*candidates: Path, default: Path) -> Path:
for path in candidates:
if path.is_file():
return path
return default
POUCH_HOME = _first_existing_dir(
PREFERRED_POUCH_HOME,
LEGACY_POUCH_HOME,
default=PREFERRED_POUCH_HOME,
)
SKILLS_DIR = POUCH_HOME / "skills"
TEMPLATE_DIR = SKILLS_DIR / "_template"
DRAFTS_DIR = POUCH_HOME / ".drafts"
CATALOG_FILE = POUCH_HOME / "catalog.yaml"
LEGACY_REGISTRY_FILE = POUCH_HOME / "registry.yaml"
CATALOG_CACHE_DIR = _first_existing_dir(
HOME / ".local" / "share" / "pouch" / "externals",
HOME / ".local" / "share" / "skills" / "externals",
default=HOME / ".local" / "share" / "pouch" / "externals",
)
CONFIG_FILE = _first_existing_file(
HOME / ".config" / "pouch" / "config.yaml",
HOME / ".config" / "skiff" / "config.yaml",
default=HOME / ".config" / "pouch" / "config.yaml",
)
SOURCES_DIR = _first_existing_dir(
HOME / ".local" / "share" / "pouch" / "sources",
HOME / ".local" / "share" / "skiff" / "sources",
default=HOME / ".local" / "share" / "pouch" / "sources",
)
AGENT_GLOBAL: dict[str, Path] = {
"cursor": HOME / ".cursor" / "skills",
"claude": HOME / ".claude" / "skills",
"codex": HOME / ".codex" / "skills",
# agents 标准目录:OMP 原生 canonical(agents provider),cursor/codex 项目级同路径
"agents": HOME / ".agents" / "skills",
}
AGENT_PROJECT: dict[str, str] = {
"cursor": ".agents/skills",
"claude": ".claude/skills",
"codex": ".agents/skills",
# 与 cursor/codex 共用 .agents/skills;软链幂等,同路径只写一次
"agents": ".agents/skills",
}
ALL_TARGETS = ("cursor", "claude", "codex", "agents")
def project_manifest(root: Path) -> Path:
"""项目清单路径:优先 .pouch.yaml,否则沿用 .skills.yaml。"""
preferred = root / PROJECT_MANIFEST
legacy = root / LEGACY_PROJECT_MANIFEST
if preferred.is_file():
return preferred
if legacy.is_file():
return legacy
return preferred
def resolve_targets(target: str | None) -> list[str]:
if target is None or target == "all":
return list(ALL_TARGETS)
if target not in ALL_TARGETS:
raise ValueError(f"未知 target: {target!r},可选: {', '.join(ALL_TARGETS)}, all")
return [target]
def agent_skill_dir(target: str, *, project_root: Path | None = None) -> Path:
if project_root is None:
return AGENT_GLOBAL[target]
return project_root / AGENT_PROJECT[target]
def ensure_pouch_home() -> None:
if not POUCH_HOME.is_dir():
raise SystemExit(
"~/.pouch 不存在。请将本仓库克隆到 ~/.pouch。"
"若仍是 ~/.skills,先执行: mv ~/.skills ~/.pouch"
)
+10 -10
View File
@@ -1,22 +1,22 @@
"""项目级 .skills.yaml 管理。"""
"""项目级 .pouch.yaml 管理。"""
from __future__ import annotations
from pathlib import Path
from typing import Any
from skiff import yaml_io
from skiff.paths import PROJECT_MANIFEST
from skiff.skills import normalize_source, resolve_skill_source
from pouch import yaml_io
from pouch.paths import project_manifest
from pouch.skills import normalize_source, resolve_skill_source
def load_manifest(path: Path | None = None) -> tuple[Path, dict[str, Any]]:
path = path or Path.cwd() / PROJECT_MANIFEST
path = path or project_manifest(Path.cwd())
if not path.is_file():
return path, {"skills": []}
data = yaml_io.safe_load(path.read_text(encoding="utf-8")) or {}
if not isinstance(data, dict):
raise SystemExit(f".skills.yaml 格式错误: {path}")
raise SystemExit(f".pouch.yaml 格式错误: {path}")
if "skills" not in data:
data["skills"] = []
return path, data
@@ -34,7 +34,7 @@ def normalize_skill_entry(entry: str | dict[str, Any]) -> dict[str, Any]:
return {"name": entry, "source": "builtin"}
name = entry.get("name")
if not name:
raise SystemExit(f".skills.yaml 条目缺少 name: {entry}")
raise SystemExit(f".pouch.yaml 条目缺少 name: {entry}")
source = normalize_source(str(entry.get("source", "builtin")))
if source == "catalog" and entry.get("registry"):
source = f"catalog:{entry['registry']}"
@@ -58,7 +58,7 @@ def add_skill_to_manifest(
) -> None:
path = manifest_path
if path.is_dir():
path = path / PROJECT_MANIFEST
path = project_manifest(path)
file_path, data = load_manifest(path) if path.is_file() else (path, {"skills": []})
if not path.is_file():
@@ -82,8 +82,8 @@ def add_skill_to_manifest(
def remove_skill_from_manifest(manifest_path: Path, name: str) -> bool:
path = manifest_path
if not path.is_file():
path = path / PROJECT_MANIFEST
if path.is_dir() or not path.is_file():
path = project_manifest(path if path.is_dir() else path.parent)
file_path, data = load_manifest(path)
original = data.get("skills", [])
kept = [e for e in original if normalize_skill_entry(e)["name"] != name]
+1 -1
View File
@@ -165,7 +165,7 @@ def select_skills(
)
footer = (
f"已选择 {len(selected)} 项 · 取消选择不卸载,卸载用 skiff remove"
f"已选择 {len(selected)} 项 · 取消选择不卸载,卸载用 pouch remove"
)
stdscr.addnstr(
height - 1,
+8 -8
View File
@@ -5,13 +5,13 @@ from __future__ import annotations
import re
from pathlib import Path
from skiff.paths import SKILLS_DIR, TEMPLATE_DIR, ensure_skills_home
from skiff.catalog import (
from pouch.paths import SKILLS_DIR, TEMPLATE_DIR, ensure_pouch_home
from pouch.catalog import (
catalog_skill_path,
discover_catalog_skills,
load_catalog,
)
from skiff.sources import (
from pouch.sources import (
discover_source_skills,
list_source_skills,
load_sources,
@@ -19,7 +19,7 @@ from skiff.sources import (
def list_builtin_skills() -> list[str]:
ensure_skills_home()
ensure_pouch_home()
if not SKILLS_DIR.is_dir():
return []
names: list[str] = []
@@ -76,7 +76,7 @@ def list_custom_skills(source: str | None = None) -> dict[str, list[str]]:
def resolve_skill_source(name: str, *, source: str | None = None) -> tuple[Path, str]:
"""返回 (skill_path, source),未指定来源时拒绝同名歧义。"""
ensure_skills_home()
ensure_pouch_home()
name, source = split_skill_spec(name, source)
builtin = SKILLS_DIR / name
@@ -106,7 +106,7 @@ def resolve_skill_source(name: str, *, source: str | None = None) -> tuple[Path,
if not (path / "SKILL.md").is_file():
raise SystemExit(
f"catalog skill {name!r} 尚未 fetch 或 path 中缺少 SKILL.md。"
f"请运行: skiff fetch {name}"
f"请运行: pouch fetch {name}"
)
return path, f"catalog:{name}"
@@ -114,7 +114,7 @@ def resolve_skill_source(name: str, *, source: str | None = None) -> tuple[Path,
if source not in sources:
raise SystemExit(
f"项目依赖 source {source!r},但本机尚未配置。"
f"请运行: skiff source add {source} <repo>"
f"请运行: pouch source add {source} <repo>"
)
skills = discover_source_skills(source, sources[source])
if name not in skills:
@@ -156,7 +156,7 @@ def resolve_skill_source(name: str, *, source: str | None = None) -> tuple[Path,
path, resolved_source = candidates[0]
if resolved_source.startswith("catalog:") and not path.exists():
provider = resolved_source.split(":", 1)[1]
raise SystemExit(f"catalog source {provider!r} 尚未 fetch。请先运行: skiff fetch {provider}")
raise SystemExit(f"catalog source {provider!r} 尚未 fetch。请先运行: pouch fetch {provider}")
return path, resolved_source
raise SystemExit(f"找不到 skill: {name}")
+26 -26
View File
@@ -2,7 +2,7 @@
## 背景
skiff 当前使用 `owned` 表示本仓库 `skills/` 中维护的 Skill,同时还支持
pouch 当前使用 `owned` 表示本仓库 `skills/` 中维护的 Skill,同时还支持
`registry` 外部仓库和用户配置的 custom source。
这些名称混合了不同维度:
@@ -17,8 +17,8 @@ skiff 当前使用 `owned` 表示本仓库 `skills/` 中维护的 Skill,同时
本文建议将用户可见的来源注册方式统一为三类:
- `builtin`:随当前 skiff 仓库提供的 Skill。
- `catalog:<name>`:由 skiff 自带目录预先登记的来源。
- `builtin`:随当前 pouch 仓库提供的 Skill。
- `catalog:<name>`:由 pouch 自带目录预先登记的来源。
- `custom:<name>`:用户在本机显式配置的命名来源。
来源注册方式与获取方式、仓库布局相互独立。builtin、catalog 和 custom 中的任意
@@ -29,20 +29,20 @@ Git 仓库或本地目录。
```mermaid
flowchart TD
S[skiff 可发现的 Skills]
S[pouch 可发现的 Skills]
S --> O["owned<br/>~/.skills/skills/*"]
S --> C["custom source<br/>~/.config/skiff/config.yaml"]
S --> O["owned<br/>~/.pouch/skills/*"]
S --> C["custom source<br/>~/.config/pouch/config.yaml"]
S --> R["registry<br/>registry.yaml"]
O --> O1["本仓库维护<br/>随 skiff 一起分发"]
O --> O1["本仓库维护<br/>随 pouch 一起分发"]
C --> C1["本地目录<br/>--local PATH"]
C --> C2["指定 Git 仓库<br/>repo + skills_path"]
R --> R1["外部单 Skill 仓库"]
R --> R2["外部 Skill Collection"]
R1 --> E["~/.local/share/skills/externals/"]
R1 --> E["~/.local/share/pouch/externals/"]
R2 --> E
```
@@ -51,7 +51,7 @@ flowchart TD
- `list` 和 `status` 支持 owned、registry 和 custom source。
- `resolve_skill_source` 可以解析三种来源并处理同名歧义。
- `select` 只组装 owned 和 registry 条目,尚未展示 custom source。
- `.skills.yaml` 默认将未声明来源的 Skill 解释为 `owned`。
- `.pouch.yaml` 默认将未声明来源的 Skill 解释为 `owned`。
- custom source 的 `skills_path` 已经可以包含多个 Skill,本质上也是 collection。
## 现行模型
@@ -61,11 +61,11 @@ flowchart TD
A[Skill Provider]
A --> B["builtin<br/>仓库内隐式注册"]
A --> C["catalog:waza<br/>skiff 预置目录"]
A --> C["catalog:waza<br/>pouch 预置目录"]
A --> D["custom:company<br/>用户本机配置"]
B --> B1["skills/ack"]
B --> B2["skills/skiff"]
B --> B2["skills/pouch"]
B --> B3["skills/builder"]
C --> C1["Git 或本地目录"]
@@ -85,14 +85,14 @@ flowchart TD
| 类型 | 含义 | 配置来源 | 用户界面展示 |
| --- | --- | --- | --- |
| `builtin` | 随当前 skiff 仓库提供 | `skills/` | `builtin` |
| `catalog` | skiff 预先登记、所有用户可发现的来源 | `catalog.yaml` | `catalog:<name>` |
| `custom` | 用户在本机显式注册的命名来源 | `~/.config/skiff/config.yaml` | `custom:<name>` |
| `builtin` | 随当前 pouch 仓库提供 | `skills/` | `builtin` |
| `catalog` | pouch 预先登记、所有用户可发现的来源 | `catalog.yaml` | `catalog:<name>` |
| `custom` | 用户在本机显式注册的命名来源 | `~/.config/pouch/config.yaml` | `custom:<name>` |
`builtin` 比 `owned` 更适合作为用户可见名称,因为它表达 Skill 的分发位置和可用
方式,而不是仓库的所有权关系。
`catalog` 比 `registry` 更准确:当前文件只是 skiff 随仓库维护的预置来源目录,
`catalog` 比 `registry` 更准确:当前文件只是 pouch 随仓库维护的预置来源目录,
并不是远程注册中心,也不是一种 Skill 来源协议。
### 三个正交维度
@@ -128,7 +128,7 @@ layout: single | collection
## `select` 展示
`skiff select` 应同时展示 builtin、catalog 和 custom Skill:
`pouch select` 应同时展示 builtin、catalog 和 custom Skill:
```text
[ ] ack builtin
@@ -151,7 +151,7 @@ layout: single | collection
## 配置表示
新写入的 `.skills.yaml` 使用以下形式:
新写入的 `.pouch.yaml` 使用以下形式:
```yaml
skills:
@@ -172,7 +172,7 @@ custom source 在 manifest 中继续保存其逻辑名称,例如 `company`。
1. 对外文档、CLI 输出和 selector 统一使用 `builtin`、`catalog:<name>` 和
`custom:<name>`。
2. 新生成的 `.skills.yaml` 对内置 Skill 写入 `source: builtin`。
2. 新生成的 `.pouch.yaml` 对内置 Skill 写入 `source: builtin`。
3. 读取旧 manifest 时继续接受 `source: owned`,并在解析时归一化为 `builtin`。
4. CLI 参数在过渡期继续接受 `--source owned`,但帮助和输出只推荐 `builtin`。
5. 读取旧 manifest 中的 `source: registry` 和 `registry: <name>`,归一化为
@@ -188,14 +188,14 @@ custom source 在 manifest 中继续保存其逻辑名称,例如 `company`。
当前实现覆盖:
- `skiff/skills.py`:来源解析、归一化和 builtin 命名。
- `skiff/project.py`:manifest 默认值、序列化与旧值兼容。
- `skiff/sources.py`:来源保留字。
- `skiff/catalog.py`:catalog 配置、checkout 与 Skill 发现。
- `skiff/cli.py`:`list`、`status`、`add`、`select` 和输出文案。
- `skiff/selector.py`:统一 catalog/custom collection 的父子展示。
- `pouch/skills.py`:来源解析、归一化和 builtin 命名。
- `pouch/project.py`:manifest 默认值、序列化与旧值兼容。
- `pouch/sources.py`:来源保留字。
- `pouch/catalog.py`:catalog 配置、checkout 与 Skill 发现。
- `pouch/cli.py`:`list`、`status`、`add`、`select` 和输出文案。
- `pouch/selector.py`:统一 catalog/custom collection 的父子展示。
- CLI 与来源解析测试。
- 根 README、`skiff/README.md`、`skills/skiff/SKILL.md` 和相关示例。
- 根 README、`pouch/README.md`、`skills/pouch/SKILL.md` 和相关示例。
## 验证要求
@@ -212,6 +212,6 @@ custom source 在 manifest 中继续保存其逻辑名称,例如 `company`。
## 设计前提
迁移前 `registry.yaml` 的真实职责只是维护 skiff 预置的来源目录,而不是提供远程
迁移前 `registry.yaml` 的真实职责只是维护 pouch 预置的来源目录,而不是提供远程
发布、版本解析或可信签名等注册中心能力,因此现已改为 `catalog.yaml`。如果未来
实现真正的远程 registry,应单独定义其协议和与 catalog 的同步关系,不复用旧名称。
+5 -5
View File
@@ -6,14 +6,14 @@ import subprocess
from pathlib import Path
from typing import Any
from skiff import yaml_io
from skiff.paths import CONFIG_FILE, SOURCES_DIR
from pouch import yaml_io
from pouch.paths import CONFIG_FILE, SOURCES_DIR
RESERVED_SOURCES = {"builtin", "catalog", "owned", "registry"}
def validate_source_name(name: str) -> None:
from skiff.skills import validate_skill_name
from pouch.skills import validate_skill_name
validate_skill_name(name)
if name in RESERVED_SOURCES:
@@ -26,12 +26,12 @@ def load_sources(path: Path | None = None) -> dict[str, dict[str, Any]]:
return {}
data = yaml_io.safe_load(path.read_text(encoding="utf-8")) or {}
if not isinstance(data, dict):
raise SystemExit(f"skiff 配置格式错误: {path}")
raise SystemExit(f"pouch 配置格式错误: {path}")
raw = data.get("sources", {})
if raw is None:
return {}
if not isinstance(raw, dict):
raise SystemExit(f"skiff 配置 sources 格式错误: {path}")
raise SystemExit(f"pouch 配置 sources 格式错误: {path}")
return {str(name): entry for name, entry in raw.items() if isinstance(entry, dict)}
+1 -1
View File
@@ -66,7 +66,7 @@ def copy_template(src: Path, dst: Path) -> None:
def find_repo_root(start: Path | None = None) -> Path | None:
start = (start or Path.cwd()).resolve()
for directory in [start, *start.parents]:
if (directory / ".skills.yaml").is_file():
if (directory / ".pouch.yaml").is_file() or (directory / ".skills.yaml").is_file():
return directory
if (directory / "skills").is_dir() and (
(directory / "catalog.yaml").is_file()
+1 -1
View File
@@ -1,4 +1,4 @@
"""轻量 YAML 读写(覆盖 skiff 使用的子集,无第三方依赖)。"""
"""轻量 YAML 读写(覆盖 pouch 使用的子集,无第三方依赖)。"""
from __future__ import annotations
+1 -1
View File
@@ -1 +1 @@
# skiff 仅使用 Python 标准库,无第三方依赖
# pouch 仅使用 Python 标准库,无第三方依赖
-253
View File
@@ -1,253 +0,0 @@
# skiff
Agent Skills 安装与管理 CLI。纯 Python 3 实现,无第三方依赖,无需编译。
## 安装
```bash
cd /path/to/skills # 本仓库根目录
./install.sh # 软链到 ~/.local/bin/skiff
```
确保 `~/.local/bin` 在 `PATH` 中。
## 命令风格
接口对齐 [Vercel skills CLI](https://github.com/vercel-labs/skills) 的 `add` / `remove`,
统一管理 builtin skill、预置 catalog source 和用户命名的 custom source。
```bash
# 浏览可用自研 skill
skiff add --list
# 装到当前项目 / 全局
skiff add discussion-notes -a cursor -y
skiff add discussion-notes -a cursor -g -y
# 卸载
skiff remove discussion-notes -a cursor -y
skiff rm discussion-notes -g -y
# 改完 skill 后提交推送(在任意目录执行,操作 ~/.skills)
skiff publish skills/discussion-notes -m "update discussion-notes" --push
```
开发时也可直接运行:
```bash
PYTHONPATH=/path/to/skills python3 -m skiff <command>
```
## 首次安装
```bash
git clone https://git.yumee.top/laily/skills.git ~/.skills
~/.skills/install.sh
```
`install.sh` 会安装 CLI,并自动执行 `skiff bootstrap`,将本仓库的 `skiff` skill 全局软链到 Cursor、Claude Code 和 Codex。也可以随时手动重跑:
```bash
skiff bootstrap
```
## 命令参考
### 查看
| 命令 | 说明 |
|------|------|
| `skiff list [--source NAME]` | 列出所有来源或指定 source 中的 skill |
| `skiff status [--target all\|cursor\|claude\|codex\|agents]` | 安装状态总览 |
### 项目初始化
| 命令 | 说明 |
|------|------|
| `skiff bootstrap` | 将本项目的 `skiff` skill 全局安装到所有 Agent |
| `skiff update` | 在 `~/.skills` 执行 `git pull`,更新 skiff 自身 |
| `skiff init <name> [--project DIR]` | 使用 builtin skill 自带模板初始化项目状态 |
### Skill 安装
| 命令 | 说明 |
|------|------|
| `skiff add <name> [--global] [-a AGENT...] [-y]` | 安装到 Agent 目录(软链) |
| `skiff select [--global] [-a AGENT...]` | 打开终端多选界面,批量安装 skill |
| `skiff remove <name> [--global] [-a AGENT...] [-y]` | 移除软链(`rm` / `r` 别名) |
| `skiff add --list` | 列出可用 builtin skill |
| `skiff publish [paths] -m MSG [--push]` | 在 ~/.skills 内 git add/commit/push |
旧命令 `install` / `uninstall` 已移除,请改用 `add` / `remove`。
全局目标路径:
| Agent | 路径 |
|-------|------|
| cursor | `~/.cursor/skills/` |
| claude | `~/.claude/skills/` |
| codex | `~/.codex/skills/` |
| agents | `~/.agents/skills/`(agents 标准目录,覆盖 OMP) |
### 预置 Catalog Source
| 命令 | 说明 |
|------|------|
| `skiff catalog add <name> <repo-url> [--ref main] [--path .]` | 写入 `catalog.yaml` |
| `skiff fetch <name>` | 克隆或更新 catalog source checkout |
| `skiff add <name> [-g] [-a AGENT...]` | 安装 catalog 中的单个 skill 或完整 collection(缺失时自动 fetch) |
| `skiff add <collection>/<skill> [...]` | 只安装 collection 中指定的 skill |
`catalog.yaml` 条目可额外提供 `description` 和 `tags`。`description`
会显示在 `skiff select` 的候选列表中。`path` 可以直接指向含
`SKILL.md` 的单个 skill,也可以指向由多个 skill 目录组成的 collection。
collection 会自动发现下一层所有含 `SKILL.md` 的目录;`skiff add <name>`
安装全部,`skiff select` 则展开为 `<name>/<skill>` 供分别勾选。同一
`repo` 与 `ref` 共享一份 Git checkout。
### 交互式批量安装
```bash
skiff select # 当前项目,全部 Agent
skiff select -a codex # 当前项目,仅 Codex
skiff select -g # 全局安装
skiff select --project ~/code/app # 指定项目
```
使用方向键移动、空格勾选、`/` 搜索、Enter 安装,按 `q` 或 Esc
取消。普通 `skiff select` 只安装到项目,并在每一项旁只读显示各 Agent 的
全局安装状态;`skiff select -g` 只安装到全局。已经安装到目标范围的 skill
默认勾选;取消勾选不会卸载已有 skill,卸载请使用 `skiff remove`。如果全局
存在同名但指向其它来源的 skill,项目选择器会显示“全局同名冲突”。
项目模式会把成功选择的项目写入 `.skills.yaml`。非交互环境请使用
`skiff add <name>...`。使用 `-a` 限定 Agent 时,该范围会记录在对应的
skill 条目中,后续 `skiff sync` 不会扩散到其他 Agent。
### Custom source(多-skill 仓库)
公司或团队维护的仓库通常包含多个 skill。使用命名 source 接入:
```bash
# Git 仓库,默认 clone 到 ~/.local/share/skiff/sources/company
skiff source add company \
git@git.company.com:platform/agent-skills.git \
--ref main \
--skills-path internal/skills
# 或接入已有本地仓库
skiff source add company \
--local ~/code/company-agent-skills \
--skills-path skills
skiff source list
skiff source fetch company
skiff list --source company
skiff add company/code-review -g -a codex
```
| 命令 | 说明 |
|------|------|
| `skiff source add <name> <repo> [--ref REF] [--checkout PATH] [--skills-path PATH]` | 注册并克隆 Git source |
| `skiff source add <name> --local PATH [--skills-path PATH]` | 接入已有本地仓库 |
| `skiff source list` / `show <name>` | 查看 source |
| `skiff source fetch <name>` / `fetch --all` | clone 或 fast-forward 更新 |
| `skiff source remove <name>` | 移除配置并保留 checkout |
配置保存在 `~/.config/skiff/config.yaml`。Git/SSH 认证复用本机 Git 配置,
skiff 不保存 token。可以使用 `company/code-review`,也可以使用
`skiff add code-review --source company`。多个来源包含同名 skill 时,必须明确来源。
### 项目级
| 命令 | 说明 |
|------|------|
| `skiff enable <name> [--target all] [--project <dir>]` | 写入 `.skills.yaml` 并创建项目软链 |
| `skiff disable <name> [--target all] [--project <dir>]` | 从 manifest 移除并删除软链 |
| `skiff sync [--target all] [--project <dir>]` | 按 `.skills.yaml` 重建软链 |
项目目标路径:
| Agent | 路径 |
|-------|------|
| cursor | `<project>/.agents/skills/` |
| claude | `<project>/.claude/skills/` |
| codex | `<project>/.agents/skills/` |
| agents | `<project>/.agents/skills/`(与 cursor/codex 共用路径,软链幂等) |
### 脚手架与健康检查
| 命令 | 说明 |
|------|------|
| `skiff create <name> [--idea TEXT] [--from-project PATH]` | 从模板创建含 `SKILL.md`、`README.md` 的草稿 |
| `skiff check <name>` | 校验草稿或正式 skill,包括人类使用说明 |
| `skiff finalize <name>` | 校验草稿并移动到正式 `skills/` |
| `skiff doctor [--target all] [--fix]` | 检查软链健康状态,`--fix` 自动修复 |
## 常用工作流
### 新建并全局启用自研 skill
```bash
skiff create my-skill --idea "描述要解决的重复问题" --from-project .
# 由 Agent 完善草稿中的 SKILL.md 和 README.md
skiff check my-skill
skiff finalize my-skill
skiff publish skills/my-skill -m "add my-skill" --push
skiff add my-skill -a cursor -g -y
skiff doctor -a cursor
```
### 在项目中启用 skill
```bash
cd ~/code/my-app
skiff add declarative-openspec-loop -a cursor -y
```
### 添加 Catalog Source
```bash
skiff catalog add my-ext https://github.com/org/repo --ref main
skiff fetch my-ext
skiff add my-ext -g
```
## 源码结构
```
skiff/
├── __init__.py # 版本号
├── __main__.py # python3 -m skiff 入口
├── cli.py # 命令定义与调度
├── paths.py # 路径常量与 Agent 目标
├── skills.py # builtin/catalog/custom 统一解析
├── catalog.py # catalog.yaml 读写与 Skill 发现
├── sources.py # custom source 配置、发现与 Git 管理
├── project.py # .skills.yaml 管理
├── symlinks.py # 软链创建/检查/修复
└── yaml_io.py # 轻量 YAML 解析(无第三方依赖)
```
入口脚本:[../bin/skiff](../bin/skiff)
## 路径约定
| 变量 | 路径 | 说明 |
|------|------|------|
| `SKILLS_HOME` | `~/.skills` | skills 仓库(软链) |
| `SKILLS_DIR` | `~/.skills/skills/` | builtin skill 目录 |
| `CATALOG_FILE` | `~/.skills/catalog.yaml` | 预置 Skill 来源目录 |
| `CATALOG_CACHE_DIR` | `~/.local/share/skills/externals/` | catalog checkout 兼容缓存;按 repo/ref 共享 |
| `CONFIG_FILE` | `~/.config/skiff/config.yaml` | custom source 配置 |
| `SOURCES_DIR` | `~/.local/share/skiff/sources/` | custom Git source 默认 checkout |
## 注意事项
- **禁止**在 `~/.cursor/skills/` 等 Agent 目录直接创建非软链的 skill
- Claude Code 对 symlink 支持不稳定;建议对单个 skill 目录软链,不要软链整个 `~/.claude/skills/`
- 若 Agent 将软链替换为普通目录,运行 `skiff doctor --fix` 重建
## 相关文档
- [项目 README](../README.md)
- [AGENTS.md](../AGENTS.md)
-3
View File
@@ -1,3 +0,0 @@
"""skiff — Agent Skills 安装与管理 CLI。"""
__version__ = "0.6.0"
-56
View File
@@ -1,56 +0,0 @@
"""路径与 Agent 目标定义。"""
from __future__ import annotations
from pathlib import Path
HOME = Path.home()
SKILLS_HOME = HOME / ".skills"
SKILLS_DIR = SKILLS_HOME / "skills"
TEMPLATE_DIR = SKILLS_DIR / "_template"
DRAFTS_DIR = SKILLS_HOME / ".drafts"
CATALOG_FILE = SKILLS_HOME / "catalog.yaml"
LEGACY_REGISTRY_FILE = SKILLS_HOME / "registry.yaml"
CATALOG_CACHE_DIR = HOME / ".local" / "share" / "skills" / "externals"
CONFIG_FILE = HOME / ".config" / "skiff" / "config.yaml"
SOURCES_DIR = HOME / ".local" / "share" / "skiff" / "sources"
PROJECT_MANIFEST = ".skills.yaml"
AGENT_GLOBAL: dict[str, Path] = {
"cursor": HOME / ".cursor" / "skills",
"claude": HOME / ".claude" / "skills",
"codex": HOME / ".codex" / "skills",
# agents 标准目录:OMP 原生 canonical(agents provider),cursor/codex 项目级同路径
"agents": HOME / ".agents" / "skills",
}
AGENT_PROJECT: dict[str, str] = {
"cursor": ".agents/skills",
"claude": ".claude/skills",
"codex": ".agents/skills",
# 与 cursor/codex 共用 .agents/skills;软链幂等,同路径只写一次
"agents": ".agents/skills",
}
ALL_TARGETS = ("cursor", "claude", "codex", "agents")
def resolve_targets(target: str | None) -> list[str]:
if target is None or target == "all":
return list(ALL_TARGETS)
if target not in ALL_TARGETS:
raise ValueError(f"未知 target: {target!r},可选: {', '.join(ALL_TARGETS)}, all")
return [target]
def agent_skill_dir(target: str, *, project_root: Path | None = None) -> Path:
if project_root is None:
return AGENT_GLOBAL[target]
return project_root / AGENT_PROJECT[target]
def ensure_skills_home() -> None:
if not SKILLS_HOME.is_dir():
raise SystemExit(
"~/.skills 不存在。请将 skills 仓库克隆到 ~/.skills"
)
+27
View File
@@ -27,11 +27,38 @@ description: >-
2. 第二步
3. 第三步
## 运营类 skill 可选:初始化 / 检查 / 工作
需要项目配置才能工作的 skill(例如构建、部署、协作闭环)增加这三个模式:
- **初始化**(用户点名才跑):探项目 → 只建本 skill 拥有的安全空结构 → 用仓库
证据填能填的 → 列出待配置项。不覆盖已有文件,不猜密钥、主机、仓库地址。
- **检查**(只读):同一套诊断,不写文件。
- **工作**:配置不齐就停,告诉用户先初始化。禁止静默初始化。
报告格式:
```text
## <skill> 初始化:完成 | 部分完成 | 阻塞
已具备: …
待配置: 路径 + 字段 + 可粘贴示例 + 缺了会挡住哪步
工具链: …
下一步: 一句话
```
项目状态放在 skill 真正消费的位置(覆盖层、`makefile.builder`、compose),不要为了对齐
而新建一层没人读的 `.pouch/<skill>/`。`pouch init` 只用于确实有 templates/
覆盖层的 skill。需要 make 目标或发布凭据的 skill 用自己的文件名(如 `makefile.builder`、
`.env.builder`),不要占用用户的 `Makefile` 或 `.env`。
---
## 注意事项
- 约束或边界条件
- `SKILL.md` 只留模式路由、全模式安全边界,以及「若 X 则读 `references/Y.md`」。
单模式步骤不要写进正文。
## 验证
+60 -269
View File
@@ -1,30 +1,27 @@
# ACK
ACK 是一个显式调用的 Agent Skill,用三种独立角色运行工程协作闭环:
ACK 是显式调用的 Agent Skill,用三种独立角色跑工程协作闭环:
- Coordinator 拆解需求、派发任务并终检。
- Developer 实现并执行白盒验证。
- Test 独立执行黑盒复测。
- Coordinator 拆解需求、派发任务并终检
- Developer 实现并做白盒验证
- Test 独立做黑盒复测
关键约束是验证者不等于实现者。每个任务最多修复三轮,仍未通过时记录为
`leftover`,然后继续处理其它任务。
验证者不能等于实现者。每个任务最多修三轮,仍不过就记为 `leftover`,然后继续别的任务。
测试环境部署由 ACK 触发、**内部调用 deployer** 执行。发版仍写在同一份
`docs/ack/delivery.yaml`。功能或 bug 验证通过后,把黑盒用例收进
`docs/ack/regression.yaml`;之后可以单独跑回归。
测试环境由 ACK 触发、内部调用 [deployer](../deployer/README.md)。发版写在同一份 `.pouch/ack/delivery.yaml`。功能或 bug 验证通过后,黑盒用例收进 `.pouch/ack/regression.yaml`,之后可以单独跑回归。
ACK 只在用户显式调用 `/ack` 或 `$ack` 时运行。
只在你显式说 `/ack` 或 `$ack` 时运行。
## 使用场景
## 什么时候使用
| 场景 | 怎么说 | 结果 |
| --- | --- | --- |
| 初始化 | `/ack 初始化` | 生成并补全 `docs/ack/` |
| 检查 | `/ack 检查配置` | 只读校验,默认不改文件 |
| 做需求 | `/ack 处理这个需求:…` | 产品文档 + 拆任务 → 确认 → 三角色闭环 |
| 修 bug | `/ack 修这个 bug:…` 或处理飞书收件 | 短描述 + 验收 → 确认(飞书须你点「已确认」)→ 同一闭环 |
| 初始化 | `/ack 初始化` | 生成并补全 `.pouch/ack/`;缺项按统一格式列出 |
| 检查 | `/ack 检查配置` | 只读校验,默认不改文件;测试环境缺 deployer 时转交 |
| 做需求 | `/ack 处理这个需求:…` | 产品文档 + 拆任务 → 你确认 → 三角色闭环 |
| 修 bug | `/ack 修这个 bug:…` 或处理飞书收件 | 短描述 + 验收 → 你确认(飞书须你点「已确认」)→ 同一闭环 |
| 交付配置 | 说明怎么布测试环境 / 怎么发版 | 写入同一份 `delivery.yaml`;测试环境绑定 deployer |
| 运行测试环境 | `/ack 重新布测试环境` | 内部加载 deployer,布 `.skiff/deployer/<env>` |
| 运行测试环境 | `/ack 重新布测试环境` | 内部加载 deployer,布 `.pouch/deployer/<env>` |
| 运行版本发布 | `/ack 发布一个版本` | 按 `intents.release`;stable/生产仍要单独批准 |
| 回归 | `/ack 回归` | 先布测试环境,再按 `regression.yaml` 用浏览器或 API 跑 |
@@ -32,29 +29,30 @@ ACK 只在用户显式调用 `/ack` 或 `$ack` 时运行。
## 安装
全局安装:
```bash
skiff add ack -g
pouch add ack # 当前项目
pouch add ack -g # 全局
```
或只安装到当前项目:
## 使用前准备
```bash
skiff add ack
```
- 项目是 Git 仓库。
- 自动拉起 Developer / Test worker 时需要 Orca,以及项目里配置好的 Codex、Cursor、Grok 或 OMP。
- 布测试环境需要同时安装 [deployer](../deployer/README.md),并准备好 SSH 和目标机器的 Docker Compose。
- 飞书收 bug 是可选的,要在 `project.md` 里配置 `bugIntake`。
- 不要 `pouch init builder` 或 `pouch init deployer`。测试环境和发版直接对 `/ack` 说;缺 deployer 时 ACK 会转去它的初始化。
## 初始化项目
```bash
skiff init ack
skiff init ack --project ~/code/my-app
pouch init ack
pouch init ack --project ~/code/my-app
```
初始化后,项目只保存自己的 ACK 状态:
也可以直接说 `/ack 初始化`。初始化后项目只保存自己的状态:
```text
docs/ack/
.pouch/ack/
├── project.md
├── tasks.yaml
├── knowledge.yaml
@@ -62,269 +60,62 @@ docs/ack/
└── regression.yaml # 默认 cases: []
```
不会在项目中复制或链接 ACK Skill。通用规范、模板和脚本始终从已安装的 Skill
目录读取。
不会把 ACK Skill 复制进项目。规范、模板和脚本始终从已安装的 Skill 目录读取。
ACK 从当前命令指定的 `--project-root/docs/ack/` 定位项目状态,不要求在
`tasks.yaml` 中持久化 `repoPath` 或 `devWorktree`。自动 worker 的实际工作目录由
`--worktree` 指定;默认在 `--project-root` 工作,v0.19 起不再配置 `allowedWorktrees` 白名单。
旧项目如果只有 `project.md` 和 `tasks.yaml`,不要重跑初始化。说 `/ack 检查配置`,授权后再补空的 `knowledge.yaml` 等缺件。
## Skill 结构
```text
skills/ack/
├── SKILL.md
├── README.md
├── VERSION
├── references/ # 三角色规范、闭环流程和初始化说明
├── templates/ # project.md、tasks.yaml、knowledge.yaml、delivery.yaml、regression.yaml 模板和 schema
├── examples/ # 完整示例
└── scripts/ # 状态校验、任务/知识/回归选择、安全验证执行与结构化 worker launcher
```
`SKILL.md` 是 Agent 的工作流入口。`references/` 是按需读取的稳定规范;
`docs/ack/project.md` 只保存当前项目的命令、路径和权限差异;
`docs/ack/tasks.yaml` 保存当前任务状态;`docs/ack/knowledge.yaml` 保存跨任务复用、
已经独立验证的项目知识护栏。
`docs/ack/delivery.yaml` 是测试环境绑定和版本发布的唯一契约;测试环境由 ACK
内部调用 deployer,运行证据记在 `tasks.yaml.deliveryRuns`。
`docs/ack/regression.yaml` 是黑盒回归用例目录,运行证据记在
`tasks.yaml.regressionRuns`。
## 检查项目状态
Agent 会从当前 ACK Skill 目录解析校验脚本:
```bash
python3 <ack-skill-dir>/scripts/validate_tasks.py docs/ack/tasks.yaml
python3 <ack-skill-dir>/scripts/validate_knowledge.py docs/ack/knowledge.yaml \
--tasks docs/ack/tasks.yaml
python3 <ack-skill-dir>/scripts/validate_delivery.py docs/ack/delivery.yaml \
--tasks docs/ack/tasks.yaml --project-root <project-root>
python3 <ack-skill-dir>/scripts/validate_regression.py docs/ack/regression.yaml \
--tasks docs/ack/tasks.yaml
```
Coordinator 可以按当前任务上下文做确定性推荐:
```bash
python3 <ack-skill-dir>/scripts/select_tasks.py docs/ack/tasks.yaml
python3 <ack-skill-dir>/scripts/select_tasks.py docs/ack/tasks.yaml \
--task-id BUG-001
python3 <ack-skill-dir>/scripts/select_knowledge.py docs/ack/knowledge.yaml \
--component web --path web/app.py --tag long-running-service --limit 10
python3 <ack-skill-dir>/scripts/select_regression.py docs/ack/regression.yaml
python3 <ack-skill-dir>/scripts/select_regression.py docs/ack/regression.yaml \
--suite full --case-id REG-login-001
```
任务选择器会解析并执行完整任务板的内置语义校验,但只输出 `project`、`summary`、
默认可工作状态或显式 `--task-id` 命中的任务,以及这些任务引用的 receipt 和 delivery
run。默认最多 20 条,超过预算时显式失败;Agent 不应回退为把完整 `tasks.yaml` 注入
上下文。
默认 JSON 输出会同时给出固定知识引用和已解析的 `verificationTarget.path/args`;
选择器只输出数据,不执行检查。`scope.all=true` 的全项目 active 规则优先占用
`--limit`;如果全项目规则本身超过预算,选择器会显式失败,不会静默漏派。
需要执行知识项引用的检查时,只传 registry ID 给 ACK 的安全执行入口:
```bash
python3 <ack-skill-dir>/scripts/run_verification.py \
docs/ack/knowledge.yaml check-api-contract --project-root <project-root>
```
该入口会在执行前重新校验知识库,只打开一次项目根目录 fd,再从同一个 fd 逐段以
`O_NOFOLLOW` 打开知识库和检查文件;检查内容复制到匿名、尽可能 sealed 的稳定
快照,再以结构化 argv 和 `shell=False` 启动。它不接受临时命令或额外参数。
选择器输出的 path/args 只用于审阅,不应由 Agent 自行拼接执行。Runner 只读取
项目内无 symlink 的权威
`docs/ack/knowledge.yaml`,不接受替代知识文件或放宽后的项目根。检查进程的 cwd
和 `ACK_PROJECT_ROOT` 都固定到该根 fd;后者是只在检查进程存活期间有效的
`/proc/self/fd/...` 或 `/dev/fd/...` 路径。原始可读路径另放在
`ACK_PROJECT_ROOT_DISPLAY`,只能用于日志,不能用于资源访问。Runner 还提供
`ACK_VERIFICATION_REF` 和 `ACK_VERIFICATION_PATH`;检查脚本必须据此定位资源,
不能依赖 `$0` 或 `__file__` 所在目录,因为实际执行的是匿名快照。
知识先由 Developer 或 Test 作为 `candidate` 提名,经独立验证和 Coordinator gate
后才能成为 `active`。Coordinator 按路径、组件、依赖、版本和标签推荐相关知识,
确认后将固定 revision 的 `knowledgeRefs` 写入任务上下文;每轮只派发命中的少量
条目,不全量注入知识库。
旧项目只有 `project.md` 和 `tasks.yaml` 时,不要重跑初始化。由 `/ack` 检查现有
状态,获得用户授权后补一个空的 `knowledge.yaml`;如果任务板尚未声明知识库,
同时只补 `project.knowledgeFile: docs/ack/knowledge.yaml`,再运行跨文件校验。
只有 Coordinator 写 `tasks.yaml`、`knowledge.yaml` 和 `regression.yaml`。知识正文
不能作为自由 shell 执行;关键约束应继续下沉到测试、lint、CI 或正式规范。ACK
不自动修改项目的 `AGENTS.md`、`CLAUDE.md` 或其它 Agent 指令文件。
## 配置与运行交付
测试环境走 deployer 的项目内布局(通常是 `.skiff/deployer/test`),发版仍用
delivery profile。可以直接说:
```text
/ack 测试环境用项目里的 .skiff/deployer/test;发版方式以后再告诉你。
```
ACK 把测试环境写成 `intents.testEnvironment.via: deployer`,把发版写成
`intents.release` 指向的 profile。首次配置保持关闭,确认后才启用。之后可以说:
## 使用示例
```text
/ack 初始化
/ack 检查配置
/ack 处理这个需求:给后台加导出 CSV
/ack 修这个 bug:保存后列表不刷新。复现:… 期望:…
/ack 处理飞书里待整理的 bug
/ack 测试环境用项目里的 .pouch/deployer/test;发版方式以后再告诉你
/ack 重新布一下测试环境,我要测试
/ack 发布一个版本
```
对应 intent 未配置时先问清楚并写回同一文件,不猜测。运行测试环境时 ACK 加载
deployer skill,不复制 compose 命令。本地进程启动写在 `project.md`,不算这个
模式。intent 运行不要求当前有 `verified` 任务;`deliveryRuns.intent` 记录是
测试环境还是发版。
交付配置只允许声明式工具 target 或仓库内可执行脚本,不接受自由 shell,也不保存
凭据值。任务进入 `verified` 后的常规交付仍按确认过的 profile 执行。默认
profile 只能停在 `validation_ready` 或 `review_ready`;稳定发布或生产部署必须经过
对应 approval 步骤。具体契约见 `references/delivery.md`。
## 启动 Worker
worker 的机器配置位于 `tasks.yaml.project.orchestration`:项目显式维护模型
allowlist、结构化 profile、默认 profile 和允许的 worktree。不得在 `project.md`
或任务里保存完整启动命令、额外 argv 或环境变量。
先审阅计划,不产生终端:
```bash
python3 <ack-skill-dir>/scripts/launch_worker.py plan \
--project-root <project-root> \
--task-id <task-id> \
--attempt-id <task-id>-A<round> \
--role developer \
--profile-id codex-dev-standard \
--worktree <allowed-worktree>
```
确认后执行 `launch`,并显式绑定刚审阅的 fingerprint:
```bash
python3 <ack-skill-dir>/scripts/launch_worker.py launch \
--project-root <project-root> \
--task-id <task-id> \
--attempt-id <task-id>-A<round> \
--role developer \
--profile-id codex-dev-standard \
--worktree <allowed-worktree> \
--expected-launch-fingerprint <plan 中的 sha256:...>
```
launcher 是自动创建 worker 的唯一入口:它从权威任务板重读 profile,构造固定
argv,忽略调用者 PATH、使用环境 allowlist,并验证真实 Git worktree 注册;然后
通过仓库外的单次启动记录、terminal-bound nonce/proof 和受限 bootstrap 调用 Orca。
返回的 receipt 含 `runtimeId`、handle、incarnation、profile hash、slot 和 worktree
identity。Coordinator 将 receipt 追加到顶层 `workerReceipts`,再把 receipt ID
写入任务的 `dispatch.developer` 或 `dispatch.test`,并同步写入本轮 `attemptId`。
校验器要求 receipt 与当前 ACK task、角色、profile 和 attempt 完全一致;历史 receipt
不能跨任务或跨轮次改挂。
v0.10 自动 launcher 仅支持 `read-only` 与 `workspace-write`。full-access、
Codex bypass、Cursor YOLO/force、Grok `--yolo` / bypassPermissions 和关闭
sandbox 都会 fail closed;在有可信平台审批或独立签发通道之前,不用项目文件
伪装成用户授权。Grok worker 由 launcher 固定带 `--always-approve`,避免工具调用
停在确认框,sandbox 仍必须启用。v0.17 起 `cli: grok` 是一等 worker CLI;`cli: omp` 使用 OMP 的结构化模型、thinking 和 approval-mode 参数,workspace-write 默认 yolo。旧任务板没有结构化
`project.orchestration` 时仍可读取和手动协作,但不得自动创建 worker。
持久化 `receiptHash` 是无密钥 checksum,不是 launcher 身份证明。ACK 只复用同一轮
内明确空闲、角色/profile/worktree 匹配,并且可以可信清理历史消息、取得新会话身份的
worker;正在执行、等待回报、状态不明或关联未完成任务的 worker 不复用。由于当前
Orca 没有可验证的历史消息清理接口,Orca 派发仍重新 `plan` 并用 expected fingerprint
启动 fresh worker。
fingerprint 只校验完整计划没有漂移,不是一次性令牌;成功后不得用同一 fingerprint
重复启动,结果不确定时必须先 reconcile。
若创建或关闭回执不完整,或外部 launch record 状态无法可靠持久化,launcher 会返回
`indeterminate/reconcile-required`;必须先核对 record 与 Orca live state,不能
直接重试。
Coordinator 最后标记整轮任务完成后,会关闭所有只关联 `verified` 任务的 worker
终端。仍关联 `blocked`、`failed_retest`、`leftover`、其它未完成任务或未解决环境事件
的终端继续保留且不设置 TTL;receipt 与测试证据不会随终端删除。
## 开始一个需求
初始化完成后可以直接说:
```text
/ack 处理这个需求:<一句话需求>
```
Coordinator 会先读取项目状态和 `references/kickoff.md`,生成产品文档、任务拆分与
可观测验收信号;用户确认后才派发实现和复测。任务 `verified` 后会把本轮黑盒路径
收进 `docs/ack/regression.yaml`,确认后才写入。
## 修复 bug
可以直接说:
```text
/ack 修这个 bug:<现象、复现、期望>
```
Coordinator 写短问题说明、复现步骤和可观测验收,不写大 PRD;你确认后再派发。
Developer 先补会失败的用例再修,后续闭环与做需求相同。
配了飞书 `bugIntake` 时,飞书是审核前的唯一协作区。你只需维护标题、详细描述和
附件;Coordinator 整理问题说明、期望效果和验收标准并写回飞书。你针对当前
revision 审核通过,并亲自把状态改成「已确认」之前,不创建任务、不派 worker。
```text
/ack 处理飞书里待整理的 bug
```
## 运行回归
平时做需求和修 bug 结束后,ACK 按本轮内容更新 `docs/ack/regression.yaml`。
之后可以单独跑:
```text
/ack 回归
/ack 跑 full 回归
/ack 回归 REG-login-001
/ack 修这些回归失败
```
ACK 先按 deployer 布测试环境,再派独立 Test 按用例用浏览器或 API 执行。失败只
报告,不会自动开修;要修再说 `/ack 修这些回归失败`。默认跑 smoke。细则见
`references/regression.md`。
首次配置交付可以说:
首次配发版也可以一次说清:
```text
/ack 更新项目交付配置:用 make build-deb 构建 DEB,发布到 preview APT 仓库,
部署到 test-server 并跑健康检查;完成后创建 PR,停在 review_ready 给我审核。
```
之后处理需求时只需在确认计划中选择 profile:
之后做需求时只要在确认计划里选 profile:
```text
/ack 处理这个需求:<一句话需求>。任务验证通过后执行 review profile。
/ack 处理这个需求:…。任务验证通过后执行 review profile。
```
ACK 会自动读取 `delivery.yaml`,无需再逐步提醒它构建、上传、部署或开 PR;目标或
权限发生漂移、缺少凭据、进入 stable/production approval 时才停下来请求决策。
## Agent 会做什么
1. 初始化或检查 `.pouch/ack/`,缺项按统一格式列出;不从 README/CI 猜测并启用交付或知识。
2. 做需求:先写产品文档、任务拆分和可观测验收;修 bug 只写短说明、复现和验收。你确认后再派 Developer 和独立 Test。
3. 需要测试环境时加载 deployer,不自己编 compose,也不把本地 `npm run dev` 当成测试环境。
4. 任务 `verified` 后提议把本轮黑盒路径写入回归目录,你确认后才写。
5. 发版读同一份 `delivery.yaml`。默认最多停在 `validation_ready` 或 `review_ready`;稳定发布或生产部署必须再经过对应批准。
6. 回归失败只报告,不会自动开修。
7. 未获授权不提交、不推送、不创建终端、不部署,也不改项目的 `AGENTS.md` / `CLAUDE.md`。
飞书收件时,你维护标题、详细描述和附件。Coordinator 整理问题说明、期望和验收并写回飞书。你针对当前 revision 审核通过,并亲自把状态改成「已确认」之前,不创建任务、不派 worker。
## 如何判断完成
- 初始化:五个状态文件齐,报告为完成,或列出待配置项和下一步。
- 需求 / bug:任务变为 `verified`,或三轮后记为 `leftover`;回归目录有本轮用例,或明确「无回归」。
- 测试环境:目标环境容器 Up,健康检查通过。
- 发版:停在约定档位;stable / 生产有单独批准记录。
- 回归:列出通过和失败的用例;失败不会被自动拿去修。
## 版本
当前 Skill 版本见 `VERSION`。新项目在 `tasks.yaml` 中以合法 SemVer 记录
`ackVersion`。从 `0.10.0` 起,`project.orchestration` 与顶层 `workerReceipts` 必须
同时存在;从 `0.11.0` 起,新项目还会生成默认关闭的 `delivery.yaml`,并在任务板声明
`project.deliveryFile` 与 `deliveryRuns`;从 `0.13.0` 起,Coordinator 使用
`select_tasks.py` 获取有预算的任务上下文,不再把完整任务板注入模型;从 `0.16.0` 起,
`delivery.yaml` 可用 `intents.testEnvironment` 与 `intents.release` 把测试环境部署和
版本发布写成用户可单独触发的操作;从 `0.17.0` 起,结构化 worker 路由支持
`cli: grok`(与 Codex、Cursor 并列);从 `0.18.0` 起支持 OMP 的
`cli: omp` profile(精确 provider/model、thinking 与 approval-mode);从 `0.17.1` 起 Grok worker argv 固定带
`--always-approve`,sandbox 仍必开;从 `0.19.0` 起 `intents.testEnvironment` 改为
deployer 绑定,ACK 内部调用 deployer skill 布测试环境,并增加
`docs/ack/regression.yaml` 与「运行回归」模式。旧的测试环境 profile ID 字符串不再
执行,需要迁到 `{via: deployer, env: <env>}`。旧项目可以不迁移回归目录而继续使用
原闭环。旧项目的 `kitVersion` 可以继续读取,但建议迁移为 `ackVersion`。
当前 Skill 版本见 `VERSION`。新项目在 `tasks.yaml` 里记 `ackVersion`。
从 0.19.0 起,测试环境写成 `{via: deployer, env: <env>}`,回归目录是 `.pouch/ack/regression.yaml`。旧的测试环境 profile ID 字符串不再执行,需要迁过去。旧项目可以不迁回归目录,继续用原来的闭环。旧的 `kitVersion` 仍可读,建议改成 `ackVersion`。
+92 -311
View File
@@ -1,325 +1,106 @@
---
name: ack
description: >-
初始化、检查并运行 ACK 三角色协作闭环。仅在用户显式调用 /ack 或 $ack,并要求
初始化 ACK、检查 docs/ack 配置、按 ACK 规划需求或修复 bug、指挥
Coordinator/Developer/Test 工作,配置测试环境与发版方式,重新部署测试环境
(内部调用 deployer),发布版本,或运行回归测试时使用。
初始化、检查并运行 ACK 三角色闭环。仅在用户显式调用 /ack 或 $ack,并要求
初始化 ACK、检查 .pouch/ack、按 ACK 做需求或修 bug、指挥
Coordinator/Developer/Test、配置或运行测试环境与发版、或跑回归时使用。
---
# ACK 项目协作入口
本 Skill 是 ACK 的完整能力包:`references/` 保存通用规范,`templates/` 保存项目
状态模板,`scripts/` 保存校验工具。目标项目只在 `docs/ack/` 保存 `project.md`、
`tasks.yaml`、`knowledge.yaml`、默认关闭的 `delivery.yaml` 和空的
`regression.yaml`,不要复制或链接 Skill 内容。
项目只在 `.pouch/ack/` 保存 `project.md`、`tasks.yaml`、`knowledge.yaml`、
`delivery.yaml` 和 `regression.yaml`。不要复制 Skill 内容,不要修改 `AGENTS.md`、
`CLAUDE.md`。
开始时解析当前 `SKILL.md` 所在目录,记为 `<ack-skill-dir>`。所有通用规范、模板和
脚本都相对此目录访问,不依赖固定的全局安装路径。
开始时解析当前 `SKILL.md` 所在目录为 `<ack-skill-dir>`。优先
`git rev-parse --show-toplevel` 作为项目根。
## 选择模式
- 用户要求初始化、接入或安装 ACK:执行“初始化”。
- 用户要求检查 ACK 是否可用、配置是否完整:执行“检查”。
- 用户要求用 ACK 做需求、修复 bug 或继续任务:执行“工作”。修 bug 不写大 PRD;
飞书收件仍走本模式。
- 用户用自然语言说明怎么部署测试环境、怎么发布版本,或要求增加、修改、关闭交付
流程:执行“交付配置维护”。测试环境绑定 deployer,发版写在同一份
`docs/ack/delivery.yaml`。
- 用户要求部署、重新部署测试环境,或按已配置方式开始测试:执行“运行测试环境”。
内部加载 deployer skill,不在 ACK 里复制 compose/rsync 命令。
- 用户要求发布版本:执行“运行版本发布”。
- 用户要求回归、跑回归测试:执行“运行回归”。先布测试环境,再派 Test 按
`docs/ack/regression.yaml` 用浏览器或 API 执行。
始终先解析真实项目根目录。优先使用 `git rev-parse --show-toplevel`;不是 Git
项目时使用用户指定目录或当前目录。不要修改项目的 `AGENTS.md`、`CLAUDE.md`
或其它 Agent 指令文件。
## 初始化
1. 确认 `skiff` 可执行,并检查 `<project>/docs/ack` 是否存在。
2. 不存在时执行:
```bash
skiff init ack --project <project-root>
```
该命令从本 Skill 的 `templates/` 生成项目状态,不会在项目中创建 Skill
软链接或资源副本。
3. 如果 `docs/ack` 已存在,不重复初始化、不覆盖文件;转入“检查”。旧项目只有
`project.md` 与 `tasks.yaml` 时,先报告缺少 `knowledge.yaml`。用户授权后,
从 `templates/knowledge.template.yaml` 生成这个缺失文件并替换项目名和时间;若
`tasks.yaml` 尚无 `project.knowledgeFile`,同时只补
`docs/ack/knowledge.yaml` 这一项。不要重跑 `skiff init`,也不要改写其它已有
项目状态。
4. 读取项目的公开配置和文档,例如 README、语言清单、包管理清单、测试配置与
CI,确定项目名、技术栈、源码/规格/测试路径及真实可执行命令。
5. 完善 `docs/ack/project.md`:
- 用实际项目值替换全部占位符。
- 无服务地址时把 Base URL 写为 `n/a`,不要虚构端口。
- 无法从项目证据确定的命令写为 `n/a`,并在结果中列为待配置项。
- 只写项目差异,不复制 `references/` 中的通用规范。
6. 完善 `docs/ack/tasks.yaml` 的项目信息。纯初始化且用户没有提供真实任务时,
删除模板示例任务并保留 `tasks: []`;不要虚构需求或缺陷。
项目状态固定从当前项目根的 `docs/ack/` 推导,不写入 `repoPath` 或 `devWorktree`;
worker 默认在 `--project-root`(权威状态目录)工作,不再配置
`allowedWorktrees` 白名单(v0.19 起废弃);需要隔离 worktree 时由 Coordinator 在
派发时显式指定。旧任务板中的 `repoPath`、`devWorktree` 仅兼容读取。
7. 检查 `docs/ack/knowledge.yaml`。新项目没有已验证的项目经验时保留
`verificationRegistry: {}` 与 `entries: []`,不从聊天、README 或单次失败中
猜测并激活知识。
8. 检查 `docs/ack/delivery.yaml`。新项目保留 `enabled: false`、空能力表和空 profile;
不从 README 或 CI 猜测、启用交付。旧项目没有该文件时仍可继续使用原 ACK
闭环;只有用户明确要求配置交付时,才按“交付配置维护”补齐。
9. 检查 `docs/ack/regression.yaml`。新项目保留 `cases: []`。旧项目没有该文件时仍
可继续原闭环;用户授权后从 `templates/regression.template.yaml` 生成,并只补
`project.regressionFile` 与顶层 `regressionRuns: []`。不要从聊天虚构用例。
10. 更新 `updatedAt`,并运行:
```bash
python3 <ack-skill-dir>/scripts/validate_tasks.py docs/ack/tasks.yaml
python3 <ack-skill-dir>/scripts/validate_knowledge.py docs/ack/knowledge.yaml \
--tasks docs/ack/tasks.yaml
python3 <ack-skill-dir>/scripts/validate_delivery.py docs/ack/delivery.yaml \
--tasks docs/ack/tasks.yaml --project-root <project-root>
python3 <ack-skill-dir>/scripts/validate_regression.py docs/ack/regression.yaml \
--tasks docs/ack/tasks.yaml
```
11. 检查 `project.md`、`tasks.yaml`、`knowledge.yaml`、`delivery.yaml` 与
`regression.yaml` 是否仍有 `<...>` 占位符。
结构校验通过且必填项目事实完整时才称“初始化完成”;否则称“部分完成”并列出
缺失值。
12. 报告创建的路径、检测到的命令、校验结果和下一步。除非用户明确要求,不提交、
不推送。
## 检查
1. 检查以下路径:
- `docs/ack/project.md`
- `docs/ack/tasks.yaml`
- `docs/ack/knowledge.yaml`
- `docs/ack/delivery.yaml`(旧项目可无;存在或被任务板引用时必须校验)
- `docs/ack/regression.yaml`(旧项目可无;存在或被任务板引用时必须校验)
需要查看任务内容时,使用 `<ack-skill-dir>/scripts/select_tasks.py` 解析完整任务板并
只输出项目配置、摘要和可工作任务;不要用 `cat`、整文件 `sed` 或等价方式把完整
`tasks.yaml` 注入上下文。完整性仍由校验器检查。
2. 读取 `<ack-skill-dir>/VERSION`,对比 `tasks.yaml` 的 `ackVersion`。旧项目只有
`kitVersion` 时仍可读取,但建议迁移为 `ackVersion`。`ackVersion` 必须是合法
SemVer;从 `0.10.0` 起 `project.orchestration` 与顶层 `workerReceipts` 必须同时
存在。
3. 查找未替换占位符,并核对项目根、覆盖层路径、Developer 白盒命令、Test
黑盒命令和 Base URL。
4. 使用 `<ack-skill-dir>/scripts/validate_tasks.py` 校验任务板,使用
`<ack-skill-dir>/scripts/validate_knowledge.py docs/ack/knowledge.yaml --tasks
docs/ack/tasks.yaml` 校验项目知识和跨文件引用。如果存在交付配置或任务板声明了
`project.deliveryFile`,再使用 `<ack-skill-dir>/scripts/validate_delivery.py
docs/ack/delivery.yaml --tasks docs/ack/tasks.yaml --project-root <project-root>`
校验交付能力、顺序、安全边界和跨文件引用。如果存在回归目录或任务板声明了
`project.regressionFile`,再使用 `<ack-skill-dir>/scripts/validate_regression.py
docs/ack/regression.yaml --tasks docs/ack/tasks.yaml` 校验用例与跨文件引用。
只报告证据明确的问题,不因旧项目缺少可选交付或回归配置而宣称失败。
5. 若存在 `project.bugIntake`,运行
`python3 <ack-skill-dir>/scripts/feishu_bug_intake.py check docs/ack/tasks.yaml`。
它只接受 `feishu-base` 和显式 profile;详细的飞书配置、凭据初始化和读取方式见
`references/feishu-bug-intake.md`。
6. 检查知识引用能解析到固定 revision,candidate 仍留在任务证据中,且
`stale`、`superseded` 和 `archived` 不会被当作可派发的 `active` 知识。
7. 若存在 `project.orchestration`,检查 profile、model allowlist、默认 profile、
允许 worktree、顶层 `workerReceipts` 与 `dispatch.developer/test` 的引用;receipt
必须绑定当前 ACK task、同一 role/profile/attempt,`receiptId` 与 `attemptId`
必须同时为空或同时填写。
缺少结构化路由的旧任务板只能使用手动模式,不能自动创建 worker。
8. 检查不会自动修复或覆盖现有配置;用户明确要求修复后再修改。
## 工作
1. 若 `docs/ack` 不存在,停止并建议先用 `/ack` 初始化;不要静默初始化。
2. 依次读取:
- `docs/ack/project.md`
- 运行 `python3 <ack-skill-dir>/scripts/select_tasks.py docs/ack/tasks.yaml`,只读取
`project`、`summary` 和默认可工作状态的任务;已知当前任务时传
`--task-id <ack-task-id>`。选择器会解析并校验完整任务板,并只附带选中任务引用的
receipt 与 delivery run。命中超过默认预算时用 `--task-id` / `--status` 缩小,
不直接回退为输出完整 `tasks.yaml`。
- 通过 `<ack-skill-dir>/scripts/select_knowledge.py` 从
`docs/ack/knowledge.yaml` 选择的当前任务相关 `active` 条目
- `<ack-skill-dir>/references/kickoff.md`
- kickoff 指定且与当前任务相关的 references 文件
- 若 `tasks.yaml.project.deliveryFile` 存在,再读取该 `delivery.yaml` 和
`<ack-skill-dir>/references/delivery.md`
- 若 `tasks.yaml.project.regressionFile` 存在,再读取该 `regression.yaml` 和
`<ack-skill-dir>/references/regression.md`
3. 当前会话担任 Coordinator,遵守项目覆盖层中的命令、路径权限、模型路由和
worker 启动规则。项目覆盖层优先于通用示例命令。按 scope 推荐相关 `active`
知识,经确认后把固定 revision 的显式 `knowledgeRefs` 写入当前任务上下文;
不全量注入知识库。
`project.bugIntake.workflow` 为 `clarified-writeback-v1`(推荐)或
`reviewed-writeback-v1`(兼容旧项目)时,按
`references/feishu-bug-intake.md` 把飞书作为审核前的唯一协作区:先运行 check/plan,
读取用户填写的 Bug。新工作流中,用户只维护标题、详细描述和附件;Coordinator 根据
来源事实与项目上下文整理问题说明、期望效果和可观测验收标准,不在收件箱写修复逻辑,
只通过安全适配器写回同一飞书记录并回读确认。用户反馈后继续只在飞书修订。
用户针对当前 `draftRevision` 明确审核通过并亲自在飞书把状态改为 `已确认` 前,不创建
或刷新 `tasks.yaml` 任务、不启动 worker、不派发 Developer/Test,也不修改应用代码。
Coordinator 不得自行写入 `已确认`。审核通过后重新读取,要求 revision 与批准值完全
一致,才通过 `import-approved` 生成规范 `taskDraft`,原样写入最终版本、
`source.workflow`、`source.approvedRevision` 与 `source.approvedPayloadHash`;校验器重算
payload hash 通过后,再用 `mark-imported` 把最终任务 ID 与同一 revision 写回飞书,
才进入三角色闭环。未声明 workflow 的旧八字段配置只按
`read-only-v1` 兼容,不得写回;
标题、详细描述和附件是来源事实,不得把 Coordinator 推断伪装成用户原文;整行空白
记录按批次 warning 跳过。
按每条记录的 `sourceRef` 去重:仅 `open` 任务可刷新描述;
`dispatched`、`fixed_by_dev`、`retesting`、`failed_retest`、`verified`、`blocked` 和
`leftover` 只报告来源漂移,绝不覆盖;来源消失或读取失败时绝不删除已有任务。
4. 新需求先写产品文档、任务拆分与可观测验收信号,更新 `tasks.yaml` 并校验,
然后交给用户确认。修 bug 写短问题说明、复现步骤和可观测验收,不写大 PRD;
Developer 先补会失败的用例再修。若启用了交付,必须默认把 `defaultProfile`、
目标、停止点和需要审批的步骤放入同一份计划,不能静默省略。用户可明确取消
本轮交付;确认前不派发实现,也不执行交付。
5. 创建或更换 worker 时,只使用
`<ack-skill-dir>/scripts/launch_worker.py plan|launch` 读取
`tasks.yaml.project.orchestration` 的 profile。不得直接执行
`orca terminal create --command`,不得接受或拼接自由 command、额外 argv、
executable、env 或 cwd。必须先审阅 `plan.launchFingerprint`,再把它作为
`launch --expected-launch-fingerprint` 传入。派发前先寻找同一 ACK 运行内的空闲
worker;只有角色、profile、worktree 和启动身份仍完全匹配,且后端能清理历史消息、
返回可核对的新会话身份时才复用。不得复用正在工作、等待回报或状态不明的 worker;
任一条件不符、清理能力不存在或无法确认清理成功时创建 fresh worker。持久化
receipt 只作审计与 dispatch 关联,不能单独授权复用。当前 Orca 终端接口不能提供
可验证的历史消息清理,因此使用 Orca 时仍走 fresh worker。
6. 用户已确认的任务按 ACK 闭环执行:Developer 实现与白盒验证;若
`intents.testEnvironment` 已启用,Coordinator 先按「运行测试环境」拉起服务,再
派 Test 独立黑盒复测。派发后先确认 worker 真正开始执行(terminal read 确认任务
注入;卡在审批提示、未回车或额度限制时按环境失败处理并报告),等待期间用
`scripts/worker_probe.py` 滚动检查活性,不盲等 `worker_done`。Coordinator 读取
证据终检并唯一写入 `tasks.yaml`。Developer 回报 `knowledgeApplied` 和
`knowledgeCandidates`,Test 回报 `knowledgeChecks` 和 `regressionCandidates`;
`candidate` 只有在独立验证和 gate 后才能由 Coordinator 写入或激活。
任务进入 `verified` 且改了用户可见行为或 API 后,按 `references/regression.md`
给出新增/更新/退役/无回归四选一,用户确认后写入 `docs/ack/regression.yaml`,
并把 case id 记入 `regressionRefs`。Test 只提名,不写该文件。
7. 执行知识项的 `verification.ref` 时,只调用
`<ack-skill-dir>/scripts/run_verification.py docs/ack/knowledge.yaml
<verification-ref> --project-root <project-root>`。不要直接执行选择器返回的 path/args,
也不要给 runner 注入额外命令或参数。
8. 不把 `worker_done` 或 Test 自报成功直接当作完成。三轮预算只计算 Test 已对齐正确
服务、数据和工具后实际执行验收所得的产品失败;环境失败不占复验轮次,不写
`failed_retest`,而写入 `dispatch.environmentIncidents`。Coordinator 先做一次有界、
安全的恢复;事件未解决、需要用户动作或会阻断本轮时,立即向用户报告原因、影响、
已尝试动作、下一恢复动作和明确的 `userAction`;即使已自动恢复,也要在最终报告汇总。
每项最多三轮有效产品复验,仍失败才记录 `leftover` 并继续其它任务。细则见
`references/optimization-method.md` §4。
9. 关键的安全、正确性和兼容性约束应下沉为测试、lint、CI 或正式规范;
`knowledge.yaml` 只保存触发条件、原因与证据引用,不能替代可执行控制。
10. 选定任务全部进入 `verified` 后,若 `delivery.enabled: true` 且用户确认的本次计划
包含交付,按 `references/delivery.md` 顺序执行 profile,并由 Coordinator 把证据
写入 `tasks.yaml.deliveryRuns`。任务状态保持 `verified`;交付失败只改变 delivery
run,不回写成任务失败。开发或测试环境完成构建、部署和健康检查后写
`validation_ready`,并把访问地址、验证范围和用户下一步交给用户;不能停在
`verified` 却声称整轮 ACK 已结束。默认 profile 最多到 `validation_ready` 或
`review_ready`,稳定发布和生产部署必须在对应步骤再次取得明确批准。
11. Coordinator 最后标记整轮任务完成后,用 `scripts/reclaim_workers.py` 先
dry-run 审阅决策,再 `--apply` 回收所有只属于 `verified` 任务的 worker
终端,并核对关闭回执;历史 receipt 和任务证据继续保留。任何还被 `open`、`dispatched`、`fixed_by_dev`、
`retesting`、`blocked`、`failed_retest`、`leftover` 或未解决环境事件引用的终端
都保留,不设置 TTL,也不能因为同一终端还关联过 `verified` 任务而误关。若关闭
结果不确定,记录并报告,不重复关闭或伪报已回收。
## 交付配置维护
1. 读取 `references/delivery.md`、deployer skill、模板、schema、现有
`delivery.yaml`、项目构建/发布入口和 CI。测试环境写成
`intents.testEnvironment: {via: deployer, env: <env>}`,并按 deployer skill
准备 `.skiff/deployer/<env>`;不要把 compose/rsync 命令写进 ACK。发版仍指向
本文件的 profile。不要拆成第二份文档。配置只引用仓库内脚本或声明式工具
target,不保存 shell。本地 `npm run dev` / `go run` 写在 `project.md` 的
Developer 白盒命令里,不算测试环境部署。
2. 若旧项目首次启用,生成 `docs/ack/delivery.yaml`,在 `tasks.yaml.project` 增加
`deliveryFile: docs/ack/delivery.yaml`,并增加顶层 `deliveryRuns: []`;不改写其它
项目状态。首次生成保持 `enabled: false`,先展示 diff 和解析出的执行顺序。
3. 运行 delivery、tasks 和跨文件校验;需要的脚本不存在、不可执行、引用不完整或
涉及凭据正文时 fail closed。凭据只写 secret 名称,值由外部环境提供。
4. 用户确认后才把配置设为启用。配置修改只影响下一次 delivery run;已确认或正在
执行的 run 使用开始时审阅的 commit/config revision 快照,不能借当前分支修改
扩大权限。
## 运行测试环境
1. 读取 `docs/ack/delivery.yaml`、`references/delivery.md` 和 deployer skill 的
`SKILL.md`。
2. `enabled` 不为 true,或 `intents.testEnvironment` 为 null:停止,请用户说明如何
部署测试环境,转入交付配置维护。不猜测编译或启动命令。
3. `intents.testEnvironment` 必须是 `{via: deployer, env: <env>}`。若仍是旧的
profile ID 字符串:停止,展示迁移说明,转入交付配置维护。不要执行 ACK
delivery profile 来布测试环境。
4. 不要求任务已 `verified`。按 deployer 的项目内环境布局操作
`.skiff/deployer/<env>`:list 确认服务,再按服务 sync + up(或用户要求的
recreate),并用 ps/logs/健康检查验证。不要复制 deployer 脚本,不要发明
第二套 compose 命令。
5. 把访问地址交给用户或随后的 Test 黑盒。证据写入 `deliveryRuns`,
`intent: testEnvironment`,`profile` 记 `deployer-<env>`,`taskIds` 可为空。
6. 派发 Test 前若该 intent 已启用,必须先完成本步骤。deployer 未安装、环境目录
不存在或健康检查失败:fail closed,报告 `userAction`,不把环境失败写成产品
失败。
## 运行回归
1. 若 `docs/ack` 不存在,停止并建议先初始化。
2. 若没有 `docs/ack/regression.yaml` 或 `project.regressionFile`:停止,用户授权后
从模板生成空文件并只补任务板指针与 `regressionRuns: []`。
3. 用 `scripts/select_regression.py` 读取 active 用例(默认 `--suite smoke`;用户
指定 full 或 case id 时缩小范围)。没有命中用例时停止并说明先收获用例。
不要把完整 `regression.yaml` 注入上下文。细则见 `references/regression.md`。
4. 先执行「运行测试环境」。
5. 当前会话担任 Coordinator:按 Test profile 启动独立 Test worker,派发回归清单、
Base URL 和每条 case 的 surface/steps/expected。Coordinator 不亲自点浏览器或
打 API。
6. Test 按 `surface` 执行:`browser` 必须走真实交互,不得改成只打 API。逐条对照
`expected` 回报。环境失败记环境事件,不记产品失败。
7. Coordinator 终检后写入 `regressionRuns`。失败只报告,不自动派 Developer,不占
任务三轮预算。用户明确要求修复时再按修 bug 为每条失败开任务。
## 运行版本发布
1. 读取同一份 `docs/ack/delivery.yaml` 与 `references/delivery.md`。
2. `enabled` 不为 true,或 `intents.release` 为 null:停止,请用户说明如何发版,
写入同一文件后再执行。
3. 按该 profile 顺序执行。stable 发布和生产部署的 `approval` 不能用口头「发版」
代替。
4. 证据写入 `deliveryRuns`,`intent: release`;绑定了任务时 `taskIds` 仍只能引用
`verified` 任务。
- 初始化、接入 ACK:执行「初始化」。
- 检查配置是否完整:执行「检查」。
- 做需求、修 bug 或继续任务:执行「工作」。修 bug 不写大 PRD。
- 增改关闭交付、说明怎么布测试环境或发版:执行「交付配置维护」。
- 部署或重布测试环境:执行「运行测试环境」(加载 deployer,不复制 compose)。
- 发布版本:执行「运行版本发布」。回归:执行「运行回归」。
- 任务板使用 Orca 时,编排命令见 [orca-adapter.md](references/orca-adapter.md)。
## 边界
- 不修改或追加任何项目 Agent 指令文件,包括 `AGENTS.md`。
- 不在项目中维护第二份 ACK 通用规范、模板或任务 schema。
- 不猜测项目命令、服务地址、worker handle 或模型名称。
- 不把 full-access、bypass、Grok `--yolo` / bypassPermissions、关闭 sandbox
或项目内“授权”字段当成 v0.10 自动 worker 的合法配置;这些 CLI 绕过标志
当前一律 fail closed。Grok worker 由 launcher 固定带 `--always-approve`,
仍必须带 sandbox。
- OMP worker 使用结构化 `--model`、`--thinking` 和 `--approval-mode` 参数。
`--approval-mode yolo` 是 OMP worker 的审批模式,不是 CLI 绕过标志:规则层
直接允许并默认启用(workspace-write → yolo、read-only → always-ask);
仍禁止 `--auto-approve`,也不适用于 codex/cursor-agent/grok。
- 不把无密钥 `receiptHash` 或 Orca live metadata 当作旧终端的启动 attestation;
没有可信空闲状态、配置匹配和历史消息清理证明时不复用既有 worker。
- launcher 返回 `indeterminate` 或 `reconcile required` 时,不直接重试;先按
launch ID、外部 record 和 Orca live state 完成人工核对。
- 不覆盖已有 `docs/ack` 文件;除用户确认的 ACK 任务或 delivery profile 外,不擅自
提交、推送、创建终端、新 worktree、发布产物或部署。
- 只有 Coordinator 写 `tasks.yaml`、`knowledge.yaml`、`regression.yaml`、
`deliveryRuns` 和 `regressionRuns`;Developer 与 Test 只读,只能通过回报提名
或验证。`delivery.yaml` 只在显式的交付配置维护中修改。
- 不把知识正文或选择器输出拼成 shell;知识检查只能通过 `run_verification.py`
按 registry ID 执行。不自动修改 `AGENTS.md`、`CLAUDE.md` 或其它 Agent 指令文件。
- 不把完整 `tasks.yaml` 注入上下文;使用 `select_tasks.py` 获取有预算的项目与任务
视图,写回前仍运行完整任务板校验。
- 项目只保存 `docs/ack/project.md`、`docs/ack/tasks.yaml`、
`docs/ack/knowledge.yaml`、可选的 `docs/ack/delivery.yaml` 和可选的
`docs/ack/regression.yaml`;通用资源始终从当前 ACK Skill 目录读取。
- 不把完整 `regression.yaml` 注入上下文;使用 `select_regression.py` 获取有预算的
用例视图,写回前仍运行完整校验。
- 不覆盖已有 `.pouch/ack`;除用户确认的任务或 delivery profile 外,不擅自提交、推送、创建终端、新 worktree、发布或部署。
- 不猜测命令、地址、worker handle 或模型名;无法确定写 `n/a`。
- 不把完整 `tasks.yaml` / `knowledge.yaml` / `regression.yaml` 注入上下文;用对应 `select_*.py`。写回前跑完整校验。
- 只有 Coordinator 写任务板、知识库、回归目录、`deliveryRuns` 和 `regressionRuns`。`delivery.yaml` 只在「交付配置维护」中改。见 [roles-and-permissions.md](references/roles-and-permissions.md)。
- 知识检查只经 `run_verification.py` 的 registry ID;不把知识正文拼成 shell。
- 自动 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。`indeterminate` / `reconcile required` 不直接重试。
- 产品失败才占三轮;环境失败写 `environmentIncidents`。见 [optimization-method.md](references/optimization-method.md) §4。
## 初始化
1. 确认 `pouch` 可执行。`.pouch/ack` 不存在则 `pouch init ack --project <project-root>`;已存在则不覆盖、转入「检查」。
2. 按 [init-new-project.md](references/init-new-project.md) 完善项目状态。不从 README/CI 猜测并启用交付或知识;初始化 **不** 自动初始化 deployer 或 builder。
3. 运行:
```bash
python3 <ack-skill-dir>/scripts/validate_tasks.py .pouch/ack/tasks.yaml
python3 <ack-skill-dir>/scripts/validate_knowledge.py .pouch/ack/knowledge.yaml \
--tasks .pouch/ack/tasks.yaml
python3 <ack-skill-dir>/scripts/validate_delivery.py .pouch/ack/delivery.yaml \
--tasks .pouch/ack/tasks.yaml --project-root <project-root>
python3 <ack-skill-dir>/scripts/validate_regression.py .pouch/ack/regression.yaml \
--tasks .pouch/ack/tasks.yaml
```
4. 检查上述文件是否仍有 `<...>` 占位符。结构校验通过且必填项目事实完整才称「完成」。除非用户明确要求,不提交、不推送。
```text
## ack 初始化:完成 | 部分完成 | 阻塞
已具备: …
待配置: 路径 + 字段 + 可粘贴示例 + 缺了会挡住哪步
工具链: pouch …
下一步: 一句话
```
## 检查
只读,不自动修复。核对清单见 [adoption-checklist.md](references/adoption-checklist.md)。用 `select_tasks.py` 看配置;跑与「初始化」相同的四个校验器。对比 `VERSION` 与 `ackVersion`(旧 `kitVersion` 仍可读);从 `0.10.0` 起 `project.orchestration` 与 `workerReceipts` 必须同时存在。旧项目可无 delivery/regression,存在或被引用时必须校验。若有 `bugIntake` 再跑 `feishu_bug_intake.py check`。若测试环境走 deployer,只读跑其 `check.py`;未通过列入待配置,不要复制 compose 或静默初始化 deployer。用同一报告格式,标题改为 `## ack 检查:…`。
## 工作
1. `.pouch/ack` 不存在:停止并建议先 `/ack` 初始化;不要静默初始化。
2. 读 `project.md`;用 `select_tasks.py`(已知任务加 `--task-id`)和 `select_knowledge.py` 取当前任务相关 `active` 条目。超预算时缩小选择,不回退为完整 yaml。
3. 新需求或尚未确认的计划:读 [kickoff.md](references/kickoff.md)。修 bug 写短问题说明、复现和可观测验收,不写大 PRD。
4. 用户已确认后按 [closed-loop.md](references/closed-loop.md) 执行。当前会话担任 Coordinator,不亲自写代码或跑测试。
5. 存在 `project.bugIntake` 时先完成「飞书收件」。创建或更换 worker 走「启动 worker」。若 `intents.testEnvironment` 已启用,派 Test 前先走「运行测试环境」。
6. 知识 `verification.ref` 只经 `run_verification.py`。不把 `worker_done` 或 Test 自报成功当作完成。环境失败先有界恢复并报告 `userAction`;三轮产品失败记 `leftover`。
7. 任务 `verified` 且改了可见行为或 API 后,转入「运行回归」收获用例。若本次计划含交付,再走对应交付模式;默认最多到 `validation_ready` 或 `review_ready`。
8. 整轮完成后 `reclaim_workers.py` 先 dry-run 再 `--apply`,只回收仅属于 `verified` 任务的 worker。关闭结果不确定则记录,不伪报。
## 飞书收件
若存在 `project.bugIntake`,读 [feishu-bug-intake.md](references/feishu-bug-intake.md)。用户针对当前 `draftRevision` 明确审核通过并亲自把飞书状态改为 `已确认` 前:不创建或刷新任务、不启动 worker、不派发、不改应用代码。Coordinator 不得自行写入 `已确认`。未声明 workflow 的旧配置只按 `read-only-v1`,不得写回。来源消失或读取失败时不删除已有任务。
## 启动 worker
只使用 `scripts/launch_worker.py plan|launch`。不得直接 `orca terminal create --command`,不得拼接自由 command、argv、executable、env 或 cwd。读 [model-routing.md](references/model-routing.md)。先审阅 `plan.launchFingerprint`,再作为 `launch --expected-launch-fingerprint` 传入。派发文案用 [prompt-templates.md](references/prompt-templates.md)。派发后 terminal read 确认已开始;卡在审批、未回车或额度限制按环境失败处理。等待期间用 `scripts/worker_probe.py`,不盲等 `worker_done`。
## 交付配置维护
读 [delivery.md](references/delivery.md)。测试环境写成 `{via: deployer, env: <env>}`;`.pouch/deployer/<env>` 未就绪则停止并加载 deployer「初始化」,不在 ACK 里复制 compose。发版写在同一份 `delivery.yaml`,不保存 shell。本地 `npm run dev` / `go run` 不算测试环境。旧项目首次启用只补文件指针与 `deliveryRuns: []`,保持 `enabled: false`,用户确认后才启用。进行中的 run 使用开始时的 commit/config 快照。
## 运行测试环境
读 `delivery.yaml` 与 [delivery.md](references/delivery.md),并加载 deployer。`enabled` 非 true 或 intent 为 null:停止,转入交付配置维护。intent 必须是 `{via: deployer, env: <env>}`;旧 profile ID 字符串要先迁移。不要用 ACK delivery profile 布环境,不猜测启动命令。不要求任务已 `verified`。对 `.pouch/deployer/<env>` 按 deployer:list → 按服务 sync+up → 健康检查。证据写入 `deliveryRuns`(`intent: testEnvironment`,`profile: deployer-<env>`)。未安装、缺目录、`check.py` 失败或健康检查失败:fail closed,报告 `userAction`,不记产品失败。
## 运行回归
读 [regression.md](references/regression.md)。缺目录则停止;用户授权后从模板生成空文件并只补指针与 `regressionRuns: []`。用 `select_regression.py` 读 active 用例(默认 `--suite smoke`);无命中则停止。先执行「运行测试环境」,再派独立 Test worker。Coordinator 不亲自点浏览器或打 API;`browser` 不得改成只打 API。终检写入 `regressionRuns`。失败不自动派 Developer,不占三轮预算。任务 `verified` 后给出新增/更新/退役/无回归四选一,用户确认后写入;Test 只提名。
## 运行版本发布
1. 读同一份 `delivery.yaml` 与 [delivery.md](references/delivery.md)。
2. `enabled` 不为 true,或 `intents.release` 为 null:停止,先做交付配置维护。
3. 按该 profile 顺序执行。stable 发布和生产部署的 `approval` 不能用口头「发版」代替。
4. 证据写入 `deliveryRuns`,`intent: release`;绑定了任务时 `taskIds` 仍只能引用 `verified` 任务。
+8 -8
View File
@@ -2,9 +2,9 @@
> 本项目基于 ack v0.19.0。
> 通用规范由 `/ack` 从 Skill 自身的 `references/` 读取,本文件只填项目差异。
> 覆盖层文件放在 `docs/ack/project.md`,不占用 `AGENTS.md`。
> 覆盖层文件放在 `.pouch/ack/project.md`,不占用 `AGENTS.md`。
> ACK 不会自动修改 `AGENTS.md`、`CLAUDE.md` 或其它 Agent 指令文件。
> `docs/ack/` 只保存 `project.md`、`tasks.yaml`、`knowledge.yaml`、`delivery.yaml`
> `.pouch/ack/` 只保存 `project.md`、`tasks.yaml`、`knowledge.yaml`、`delivery.yaml`
> 与 `regression.yaml`。
## 项目概览
@@ -13,11 +13,11 @@
- 技术栈:`TypeScript + React (Vite) + Go`
- 运行命令:`npm run dev`(前端)、`go run ./server`(后端)
- Base URL:`http://localhost:5173`
- 任务板:`docs/ack/tasks.yaml`
- 项目知识:`docs/ack/knowledge.yaml`
- 交付契约:`docs/ack/delivery.yaml`
- 回归目录:`docs/ack/regression.yaml`
- 覆盖层文件:`docs/ack/project.md`
- 任务板:`.pouch/ack/tasks.yaml`
- 项目知识:`.pouch/ack/knowledge.yaml`
- 交付契约:`.pouch/ack/delivery.yaml`
- 回归目录:`.pouch/ack/regression.yaml`
- 覆盖层文件:`.pouch/ack/project.md`
## 稳定规范(引用,不重复)
@@ -33,7 +33,7 @@
## Worker 路由
结构化配置位于 `docs/ack/tasks.yaml` 的 `project.orchestration`,启动记录位于顶层
结构化配置位于 `.pouch/ack/tasks.yaml` 的 `project.orchestration`,启动记录位于顶层
`workerReceipts`。本项目默认使用:
| 角色 | profile ID | 档位 |
+4 -4
View File
@@ -7,10 +7,10 @@ ackVersion: "0.19.0"
project:
name: "notes-web"
baseUrl: "http://localhost:5173"
overlayFile: "docs/ack/project.md"
knowledgeFile: "docs/ack/knowledge.yaml"
deliveryFile: "docs/ack/delivery.yaml"
regressionFile: "docs/ack/regression.yaml"
overlayFile: ".pouch/ack/project.md"
knowledgeFile: ".pouch/ack/knowledge.yaml"
deliveryFile: ".pouch/ack/delivery.yaml"
regressionFile: ".pouch/ack/regression.yaml"
orchestration:
profileVersion: 1
mode: "manual"
+8 -8
View File
@@ -3,8 +3,8 @@
## 安装与初始化
- [ ] ACK Skill 已全局安装或安装到当前项目。
- [ ] 已运行 `skiff init ack --project <project-root>`。
- [ ] `docs/ack/` 只包含项目自己的 `project.md`、`tasks.yaml`、`knowledge.yaml`、
- [ ] 已运行 `pouch init ack --project <project-root>`。
- [ ] `.pouch/ack/` 只包含项目自己的 `project.md`、`tasks.yaml`、`knowledge.yaml`、
默认关闭的 `delivery.yaml` 与空的 `regression.yaml`。
- [ ] 旧项目缺少 `knowledge.yaml` 时,只补空文件及缺失的
`project.knowledgeFile` 指针,没有重跑初始化或覆盖其它项目状态。
@@ -16,10 +16,10 @@
- [ ] `project.md` 只保存项目差异,不复制 Skill 的通用规范。
- [ ] `tasks.yaml` 的 `project.overlayFile` 指向实际覆盖层。
- [ ] `tasks.yaml` 的 `project.knowledgeFile` 固定为
`docs/ack/knowledge.yaml`。
- [ ] 新项目的 `project.deliveryFile` 固定为 `docs/ack/delivery.yaml`,顶层有
`.pouch/ack/knowledge.yaml`。
- [ ] 新项目的 `project.deliveryFile` 固定为 `.pouch/ack/delivery.yaml`,顶层有
`deliveryRuns: []`;旧项目未采用交付能力时可无这两项。
- [ ] 新项目的 `project.regressionFile` 固定为 `docs/ack/regression.yaml`,顶层有
- [ ] 新项目的 `project.regressionFile` 固定为 `.pouch/ack/regression.yaml`,顶层有
`regressionRuns: []`;旧项目未采用回归能力时可无这两项。
- [ ] 技术栈、运行、构建、单测和集成测试命令均来自项目证据。
- [ ] Coordinator、Developer、Test 的模型档位和升级规则已明确。
@@ -28,7 +28,7 @@
- [ ] `allowedWorktrees` 已废弃(v0.19 起),新任务板不配置;worker 默认在
`--project-root` 工作,其它 worktree 由 launcher 按同 git 仓库且已注册约束放行。
- [ ] `tasks.yaml` 不需要保存 `repoPath` 或 `devWorktree`;项目状态从当前
`--project-root/docs/ack/` 推导,worker 路径由 `--worktree` 显式指定。
`--project-root/.pouch/ack/` 推导,worker 路径由 `--worktree` 显式指定。
## 路径权限
@@ -121,6 +121,6 @@
blocked/failed/leftover、未完成任务或未解决环境事件的终端保留且不设 TTL。
首次接入建议选择一个低风险问题跑完整闭环。项目差异写回
`docs/ack/project.md`;通用问题回流到 ACK Skill 的 `references/`、`templates/`
`.pouch/ack/project.md`;通用问题回流到 ACK Skill 的 `references/`、`templates/`
或 `scripts/`,并更新 `VERSION`。项目特有、跨任务复用且已经验证的经验才写入
`docs/ack/knowledge.yaml`。
`.pouch/ack/knowledge.yaml`。
+1 -1
View File
@@ -44,7 +44,7 @@ Coordinator 发现或读取 open 任务
直到 Developer 的 worker_done / escalation(含 knowledgeApplied / knowledgeCandidates)
-> writeback fixed_by_dev
-> 若 delivery.yaml intents.testEnvironment 已启用:Coordinator 先按 deployer
绑定拉起 `.skiff/deployer/<env>`,再派 Test;Test 不发明编译或启动命令
绑定拉起 `.pouch/deployer/<env>`,再派 Test;Test 不发明编译或启动命令
-> 为 Test 独立解析安全 profile;安全重置同角色空闲 worker,或重新 plan/launch fresh worker
-> dispatch 给 Test(retesting)
-> 确认 Test 已开始执行(terminal read 确认任务注入;未开始按环境失败处理)
+15 -14
View File
@@ -2,8 +2,8 @@
本文件定义可选的 `verified -> validation_ready/review_ready/released` 交付阶段。开发、独立复测和
Coordinator 终检仍由 ACK 原有闭环负责;只有选中的任务全部 `verified` 后才能进入
交付。项目配置位于 `docs/ack/delivery.yaml`,运行证据写入
`docs/ack/tasks.yaml.deliveryRuns`。
交付。项目配置位于 `.pouch/ack/delivery.yaml`,运行证据写入
`.pouch/ack/tasks.yaml.deliveryRuns`。
## 1. 配置与授权不是一回事
@@ -54,7 +54,7 @@ channel、environment 或 source revision 漂移时重新确认。
## 3.1 测试环境与发版写在同一份契约
`docs/ack/delivery.yaml` 是测试环境绑定和版本发布的唯一文档。不要另写操作手册,
`.pouch/ack/delivery.yaml` 是测试环境绑定和版本发布的唯一文档。不要另写操作手册,
也不要把其中一项写进 `project.md`。用户用自然语言说明「怎么布测试环境」或
「怎么发版」时,Coordinator 把两者都维护进这份文件的 `intents`。
@@ -62,17 +62,18 @@ channel、environment 或 source revision 漂移时重新确认。
intents:
testEnvironment:
via: deployer
env: test # 项目 .skiff/deployer/test;尚未说明时为 null
env: test # 项目 .pouch/deployer/test;尚未说明时为 null
release: null # profile ID,或 null
```
- `testEnvironment` 绑定 deployer skill 的项目环境目录。用户说「重新布测试环境」
「我要测试」时,ACK 加载 deployer 的 `SKILL.md`,对 `.skiff/deployer/<env>`
按服务执行 sync + up 和健康检查。派发 Test 复测或跑回归前,若该 intent 已配置
且 `enabled: true`,Coordinator 也先执行它。不要求当前有 `verified` 任务。
Test 不对这个 intent 发明编译或启动命令。旧的 profile ID 字符串不再执行,必须
迁到 `{via: deployer, env: <env>}`。本地进程启动写在 `project.md`,不算这个
intent。
「我要测试」时,ACK 加载 deployer 的 `SKILL.md`,对 `.pouch/deployer/<env>`
按服务执行 sync + up 和健康检查。环境目录不存在或 deployer `check.py` 未通过
时,加载 deployer skill 的「初始化」,不要在 ACK 里复制 compose 命令。派发
Test 复测或跑回归前,若该 intent 已配置且 `enabled: true`,Coordinator 也先
执行它。不要求当前有 `verified` 任务。Test 不对这个 intent 发明编译或启动
命令。旧的 profile ID 字符串不再执行,必须迁到 `{via: deployer, env: <env>}`。
本地进程启动写在 `project.md`,不算这个 intent。
- `release` 指向 `stopAt: released` 的 profile。用户说「发布一个版本」时执行它。
口头「发版」不能代替 stable/production 的 `approval` 步骤。
- 对应 intent 为 `null` 或交付未启用:停止,请用户说明怎么做,按「交付配置维护」
@@ -90,7 +91,7 @@ intents:
```bash
python3 <ack-skill-dir>/scripts/validate_delivery.py \
docs/ack/delivery.yaml --tasks docs/ack/tasks.yaml \
.pouch/ack/delivery.yaml --tasks .pouch/ack/tasks.yaml \
--project-root <project-root>
```
@@ -111,7 +112,7 @@ intents:
- `verify`:运行指定 entrypoint,失败即停止。
- `pull-request`:在精确 source revision 上提交、推送任务分支并创建或复用 Draft
PR/MR。普通任务使用项目已确认的 Forge 流程;只有本次是版本发布生命周期且用户
明确要求时才调用独立的 `manage-release`。没有对应能力或认证时标记 blocked,不用
明确要求时才调用独立的 `releaser`。没有对应能力或认证时标记 blocked,不用
带 token 的临时 curl 兜底。remote 与 base branch 必须来自该步骤,不能临时猜测。
- `build`:调用 artifact 的 build entrypoint。DEB 必须记录包名、版本、架构和
SHA-256;OCI image 必须记录完整引用、platform 和 digest。产物必须绑定当前 source
@@ -152,9 +153,9 @@ ID 指向不同 commit、digest 或目标时停止,不覆盖或另建伪装成
## 7. 与低层 Skill 的边界
ACK 只负责读取项目交付契约、编排顺序、守住审批点并汇总证据,不复制低层 skill 的
上传、镜像、Git 发布或远程 Compose 实现。`builder`、`manage-release` 和
上传、镜像、Git 发布或远程 Compose 实现。`builder`、`releaser` 和
`deployer` 仍是可独立使用、独立安装的能力。运行测试环境时 ACK 必须加载
deployer skill,不能把 compose/rsync/远程 docker 命令写进 ACK。发版步骤缺失
builder 或 manage-release 时,ACK 使用契约中已审查的项目 entrypoint,二者都不可用
builder 或 releaser 时,ACK 使用契约中已审查的项目 entrypoint,二者都不可用
时把对应步骤标为 `blocked`。低层 skill 自身要求显式调用时,ACK 不能绕过它的
触发与授权边界;ACK 内部调用 deployer 布测试环境是该 skill 的合法调用路径。
+9 -9
View File
@@ -9,7 +9,7 @@
## 项目配置与 Base 结构
`docs/ack/tasks.yaml` 的 `project.bugIntake` 必须声明 `provider: feishu-base`、
`.pouch/ack/tasks.yaml` 的 `project.bugIntake` 必须声明 `provider: feishu-base`、
`workflow: clarified-writeback-v1`、显式 `profile`、`baseToken`、`tableId`、`viewId` 和字段映射:
```yaml
@@ -34,8 +34,8 @@ fields:
调整当前 view 的可见字段,不删除旧列:
```bash
python3 <ack-skill-dir>/scripts/feishu_bug_intake.py schema-plan docs/ack/tasks.yaml
python3 <ack-skill-dir>/scripts/feishu_bug_intake.py schema-apply docs/ack/tasks.yaml \
python3 <ack-skill-dir>/scripts/feishu_bug_intake.py schema-plan .pouch/ack/tasks.yaml
python3 <ack-skill-dir>/scripts/feishu_bug_intake.py schema-apply .pouch/ack/tasks.yaml \
--expected-schema-fingerprint <schemaFingerprint>
```
@@ -68,11 +68,11 @@ locale;调用者环境中的凭据和运行时注入变量不会传入。不
## 读取与整理
```bash
python3 <ack-skill-dir>/scripts/feishu_bug_intake.py check docs/ack/tasks.yaml
python3 <ack-skill-dir>/scripts/feishu_bug_intake.py check .pouch/ack/tasks.yaml
tmpdir=$(mktemp -d)
python3 <ack-skill-dir>/scripts/feishu_bug_intake.py fetch docs/ack/tasks.yaml \
python3 <ack-skill-dir>/scripts/feishu_bug_intake.py fetch .pouch/ack/tasks.yaml \
--output-dir "$tmpdir"
python3 <ack-skill-dir>/scripts/feishu_bug_intake.py plan docs/ack/tasks.yaml \
python3 <ack-skill-dir>/scripts/feishu_bug_intake.py plan .pouch/ack/tasks.yaml \
--output-dir "$tmpdir"
```
@@ -101,7 +101,7 @@ Coordinator 对每条 Bug:
```bash
python3 <ack-skill-dir>/scripts/feishu_bug_intake.py write-draft \
docs/ack/tasks.yaml --record-id <record-id> \
.pouch/ack/tasks.yaml --record-id <record-id> \
--expected-source-ref <sourceRef> \
--expected-draft-revision <draftRevision> --input <draft.json>
```
@@ -120,7 +120,7 @@ python3 <ack-skill-dir>/scripts/feishu_bug_intake.py write-draft \
```bash
python3 <ack-skill-dir>/scripts/feishu_bug_intake.py import-approved \
docs/ack/tasks.yaml --record-id <record-id> \
.pouch/ack/tasks.yaml --record-id <record-id> \
--expected-source-ref <approved-sourceRef> \
--expected-draft-revision <approvedDraftRevision>
```
@@ -135,7 +135,7 @@ python3 <ack-skill-dir>/scripts/feishu_bug_intake.py import-approved \
```bash
python3 <ack-skill-dir>/scripts/feishu_bug_intake.py mark-imported \
docs/ack/tasks.yaml --record-id <record-id> --task-id <ack-task-id> \
.pouch/ack/tasks.yaml --record-id <record-id> --task-id <ack-task-id> \
--expected-source-ref <approved-sourceRef> \
--expected-draft-revision <approvedDraftRevision>
```
+36 -39
View File
@@ -9,10 +9,10 @@
1. 目标项目根目录。
2. ACK Skill 已全局安装或安装到当前项目。
3. `skiff` 命令可用。
3. `pouch` 命令可用。
不要覆盖已有的 `docs/ack/project.md`、`docs/ack/tasks.yaml`、
`docs/ack/knowledge.yaml`、`docs/ack/delivery.yaml`、`docs/ack/regression.yaml`、
不要覆盖已有的 `.pouch/ack/project.md`、`.pouch/ack/tasks.yaml`、
`.pouch/ack/knowledge.yaml`、`.pouch/ack/delivery.yaml`、`.pouch/ack/regression.yaml`、
`AGENTS.md` 或其它 Agent
指令文件。ACK 不会自动
修改 `AGENTS.md`、`CLAUDE.md` 或其它 Agent 指令文件。不要把 token、`.env`
@@ -23,19 +23,19 @@
在目标项目执行:
```bash
skiff init ack
pouch init ack
```
或显式指定项目:
```bash
skiff init ack --project <project-root>
pouch init ack --project <project-root>
```
命令从 ACK Skill 自带的 `templates/` 生成:
```text
docs/ack/
.pouch/ack/
├── project.md
├── tasks.yaml
├── knowledge.yaml
@@ -49,31 +49,31 @@ docs/ack/
### 旧项目补充知识库
旧项目已经有 `project.md` 和 `tasks.yaml`、但没有 `knowledge.yaml` 时,不要重跑
`skiff init ack`。先检查现有文件并向用户报告缺失项;用户授权后,只从
`templates/knowledge.template.yaml` 生成 `docs/ack/knowledge.yaml`,替换项目名和
`pouch init ack`。先检查现有文件并向用户报告缺失项;用户授权后,只从
`templates/knowledge.template.yaml` 生成 `.pouch/ack/knowledge.yaml`,替换项目名和
当前时间,保留 `entries: []`。如果现有任务板缺少
`project.knowledgeFile`,同一次授权只补
`knowledgeFile: docs/ack/knowledge.yaml`,不改写其它项目状态。生成后运行任务板、
`knowledgeFile: .pouch/ack/knowledge.yaml`,不改写其它项目状态。生成后运行任务板、
知识库和跨文件引用校验。
### 旧项目补充交付配置
`delivery.yaml` 对旧项目是可选能力;缺少它不会影响三角色开发与验证闭环。只有用户
明确要求配置项目交付时,才从 `templates/delivery.template.yaml` 生成文件,同时在
任务板补 `project.deliveryFile: docs/ack/delivery.yaml` 与顶层
任务板补 `project.deliveryFile: .pouch/ack/delivery.yaml` 与顶层
`deliveryRuns: []`。首次生成保持 `enabled: false`,按 `delivery.md` 展示并确认
解析结果后才启用。不要重跑 `skiff init ack`,也不要改写已有任务或知识。
解析结果后才启用。不要重跑 `pouch init ack`,也不要改写已有任务或知识。
### 旧项目补充回归目录
`regression.yaml` 对旧项目是可选能力。用户明确要求回归或授权补齐时,从
`templates/regression.template.yaml` 生成 `docs/ack/regression.yaml`,并只补
`project.regressionFile: docs/ack/regression.yaml` 与顶层 `regressionRuns: []`。
`templates/regression.template.yaml` 生成 `.pouch/ack/regression.yaml`,并只补
`project.regressionFile: .pouch/ack/regression.yaml` 与顶层 `regressionRuns: []`。
保持 `cases: []`,不要从聊天虚构用例。
## 完善项目覆盖层
编辑 `docs/ack/project.md`,填入:
编辑 `.pouch/ack/project.md`,填入:
- 项目名、技术栈、运行命令和 Base URL。
- Coordinator、Developer、Test 的实际模型档位。
@@ -85,7 +85,7 @@ docs/ack/
## 完善任务板
编辑 `docs/ack/tasks.yaml`:
编辑 `.pouch/ack/tasks.yaml`:
- `ackVersion` 使用 ACK Skill 的合法 SemVer `VERSION`;从 `0.10.0` 起
`project.orchestration` 与顶层 `workerReceipts` 必须同时存在。
@@ -93,12 +93,12 @@ docs/ack/
自动补交付配置。
- `updatedAt` 使用当前带时区时间。
- `project.name` 使用真实值;`overlayFile` 和 `knowledgeFile` 使用项目内相对路径。
ACK 从命令行 `--project-root` 下固定的 `docs/ack/` 布局解析项目状态,不把
ACK 从命令行 `--project-root` 下固定的 `.pouch/ack/` 布局解析项目状态,不把
`repoPath` 或 `devWorktree` 绝对路径写入任务板。旧任务板中的这两个字段仅兼容读取,
不再参与路径绑定。
- 新项目的 `project.deliveryFile` 固定为 `docs/ack/delivery.yaml`,并保留顶层
- 新项目的 `project.deliveryFile` 固定为 `.pouch/ack/delivery.yaml`,并保留顶层
`deliveryRuns: []`。旧项目只有在采用交付能力时才补这两个字段。
- 新项目的 `project.regressionFile` 固定为 `docs/ack/regression.yaml`,并保留顶层
- 新项目的 `project.regressionFile` 固定为 `.pouch/ack/regression.yaml`,并保留顶层
`regressionRuns: []`。旧项目只有在采用回归能力时才补这两个字段。
- `allowedWorktrees` 已废弃(v0.19 起),新任务板不生成该字段;worker 默认在
`--project-root` 工作。模型 allowlist、profiles 和 defaults 使用项目实际允许值。
@@ -111,7 +111,7 @@ docs/ack/
## 初始化项目知识
新项目的 `docs/ack/knowledge.yaml` 保持 `verificationRegistry: {}` 与
新项目的 `.pouch/ack/knowledge.yaml` 保持 `verificationRegistry: {}` 与
`entries: []`。不要从聊天、README、issue 或单次失败中猜测并激活知识。
项目运行 ACK 后,Developer 和 Test 可以通过回报提名 `knowledgeCandidates`;
@@ -127,17 +127,18 @@ candidate 留在任务证据中,不会被派发。只有 Test 独立验证且
## 初始化项目交付
新项目的 `docs/ack/delivery.yaml` 保持 `enabled: false`、空能力表、空 profile,以及
新项目的 `.pouch/ack/delivery.yaml` 保持 `enabled: false`、空能力表、空 profile,以及
`intents.testEnvironment: null` 与 `intents.release: null`。
不要根据 README 或 CI 自动推断并启用发布/部署。用户说明测试环境后,Coordinator
按 deployer skill 准备 `.skiff/deployer/<env>`,并把
`intents.testEnvironment` 写成 `{via: deployer, env: <env>}`;发版仍指向 profile。
把 `intents.testEnvironment` 写成 `{via: deployer, env: <env>}`,若
`.pouch/deployer/<env>` 尚未就绪则加载 deployer skill 的「初始化」;发版仍指向
profile。ACK 初始化不自动跑 deployer 初始化。
配置中不保存 shell、环境变量值或凭据正文;稳定发布和生产部署必须有显式
approval 步骤。
## 初始化回归目录
新项目的 `docs/ack/regression.yaml` 保持 `cases: []`。不要从 README 或聊天猜测
新项目的 `.pouch/ack/regression.yaml` 保持 `cases: []`。不要从 README 或聊天猜测
用例。任务 `verified` 后按 `regression.md` 收获。
## 校验
@@ -145,12 +146,12 @@ approval 步骤。
Agent 从当前 `SKILL.md` 解析 ACK Skill 目录后运行:
```bash
python3 <ack-skill-dir>/scripts/validate_tasks.py docs/ack/tasks.yaml
python3 <ack-skill-dir>/scripts/validate_knowledge.py docs/ack/knowledge.yaml --tasks docs/ack/tasks.yaml
python3 <ack-skill-dir>/scripts/validate_delivery.py docs/ack/delivery.yaml \
--tasks docs/ack/tasks.yaml --project-root <project-root>
python3 <ack-skill-dir>/scripts/validate_regression.py docs/ack/regression.yaml \
--tasks docs/ack/tasks.yaml
python3 <ack-skill-dir>/scripts/validate_tasks.py .pouch/ack/tasks.yaml
python3 <ack-skill-dir>/scripts/validate_knowledge.py .pouch/ack/knowledge.yaml --tasks .pouch/ack/tasks.yaml
python3 <ack-skill-dir>/scripts/validate_delivery.py .pouch/ack/delivery.yaml \
--tasks .pouch/ack/tasks.yaml --project-root <project-root>
python3 <ack-skill-dir>/scripts/validate_regression.py .pouch/ack/regression.yaml \
--tasks .pouch/ack/tasks.yaml
```
同时确认:
@@ -158,8 +159,8 @@ python3 <ack-skill-dir>/scripts/validate_regression.py docs/ack/regression.yaml
- `project.md`、`tasks.yaml`、`knowledge.yaml`、`delivery.yaml` 和
`regression.yaml` 没有未替换的 `<...>` 占位符。
- `project.overlayFile` 指向真实文件。
- `project.knowledgeFile` 指向 `docs/ack/knowledge.yaml`。
- 新项目的 `project.deliveryFile` 指向 `docs/ack/delivery.yaml`;交付默认关闭。
- `project.knowledgeFile` 指向 `.pouch/ack/knowledge.yaml`。
- 新项目的 `project.deliveryFile` 指向 `.pouch/ack/delivery.yaml`;交付默认关闭。
- Developer 与 Test 的验证命令可执行。
- `project.orchestration` 的 profile/allowlist/defaults 通过校验,自动模式只允许
`read-only` 或 `workspace-write`;旧任务板未迁移时保持手动模式。
@@ -171,12 +172,8 @@ python3 <ack-skill-dir>/scripts/validate_regression.py docs/ack/regression.yaml
## 初始化报告
完成后报告:
按 `SKILL.md`「初始化」最后一步的格式报告(`## ack 初始化:完成 | 部分完成 | 阻塞`),
列出已具备项、待配置项(路径 + 字段 + 示例)、工具链和下一步。
- 创建或确认的项目文件(含回归目录)。
- 检测到的技术栈和验证命令。
- 任务板、项目知识和交付契约校验结果。
- 仍需用户补充的值。
只有结构校验通过且必填项目事实完整时才称“初始化完成”;否则称“部分完成”,并列出
具体阻塞项。除非用户明确要求,不提交、不推送。
只有结构校验通过且必填项目事实完整时才称「完成」;否则称「部分完成」或「阻塞」。
除非用户明确要求,不提交、不推送。
+28 -39
View File
@@ -1,6 +1,8 @@
# 如何开始一个需求(Kickoff)
从零开一个需求的启动手册。角色/权限见 `roles-and-permissions.md`,闭环见 `closed-loop.md`,模型见 `model-routing.md`。
从零开一个需求的启动手册。本文件只覆盖确认前的产品文档、任务拆分与验收信号。
用户确认后的三角色闭环、飞书收件、启动 worker、交付和回归由 `SKILL.md` 按条件加载,
不要在本文件开头预读其它规范。
---
@@ -9,7 +11,7 @@
**你(发起编排的强模型会话)就是 Coordinator (PM) / 产品。** 你负责写文档、拆任务、
编排、终检,**不亲自写代码、不亲自跑测试**。开发和测试是另起的 worker agent;
具体 CLI、模型、reasoning effort 和执行模式的机器事实源是
`docs/ack/tasks.yaml` 的 `project.orchestration`。`docs/ack/project.md` 只解释项目
`.pouch/ack/tasks.yaml` 的 `project.orchestration`。`.pouch/ack/project.md` 只解释项目
差异,不能提供另一套启动命令。
---
@@ -18,19 +20,19 @@
```text
我要做一个新需求:<一句话需求>。
你作为 ack 的 Coordinator(PM),按 ACK Skill 的 references 规范执行:
你作为 ack 的 Coordinator(PM),按 ACK Skill 执行:
1. 先读 docs/ack/project.md,并用 `scripts/select_tasks.py docs/ack/tasks.yaml`
1. 先读 .pouch/ack/project.md,并用 `scripts/select_tasks.py .pouch/ack/tasks.yaml`
读取有预算的 project、summary 和可工作任务;已知任务时传 `--task-id`,不要把
完整 tasks.yaml 注入上下文。校验 docs/ack/knowledge.yaml 并用
`scripts/select_knowledge.py` 只读取当前任务相关的 active 条目,再读
references/roles-and-permissions.md、closed-loop.md、optimization-method.md。
如果 tasks.yaml 声明 project.deliveryFile,再读取 delivery.yaml 与
references/delivery.md,但不要把配置本身当作执行授权。
完整 tasks.yaml 注入上下文。校验 .pouch/ack/knowledge.yaml 并用
`scripts/select_knowledge.py` 只读取当前任务相关的 active 条目。
本 kickoff 只做确认前的产品文档与任务拆分;用户确认后的闭环、飞书、启动
worker、交付和回归按 ACK Skill 的模式路由按条件加载,不要在本步预读其它
references。配置本身不是执行授权。
2. 写产品文档到 docs/(PRD / 交互 / 验收),把需求拆成任务,每个任务的验收写成可观测信号(可见文本 / API 结果 / 交互结果)。
3. 按任务 scope 从 knowledge.yaml 推荐 active 知识,确认后把固定 revision 的
knowledgeRefs 写入任务;不要派发 candidate 或全量知识库。
4. 把任务写进 docs/ack/tasks.yaml(只有你写),校验 tasks.yaml 和 knowledge.yaml。
4. 把任务写进 .pouch/ack/tasks.yaml(只有你写),校验 tasks.yaml 和 knowledge.yaml。
5. 先把「产品文档 + 任务拆分 + 验收信号 + 适用知识引用」给我确认;若启用了交付,
同时列明本次 profile、目标、停止点与审批步骤。不要急着派发或交付。
6. 我确认后,按 ack 闭环循环:先用 `scripts/launch_worker.py` 校验结构化
@@ -41,8 +43,8 @@
dispatch 测试独立复测 → 你读证据终检 → 回写 tasks.yaml;
每个任务最多三轮有效产品复验,三轮不过记 leftover 并升级我复盘;环境失败单独
记录、恢复并告诉我下一步,不占产品复验轮次。
7. 所选任务都 verified 后,按 regression.md 给出新增/更新/退役/无回归,确认后写入
docs/ack/regression.yaml。只有本次计划包含交付时才按 profile 顺序执行并写
7. 所选任务都 verified 后,按 SKILL.md「运行回归」给出新增/更新/退役/无回归,确认后写入
.pouch/ack/regression.yaml。只有本次计划包含交付时才按 profile 顺序执行并写
deliveryRuns;启用 delivery 时不能省略 defaultProfile,默认停在 validation_ready
或 review_ready,stable/production 步骤再次向我确认。
```
@@ -52,15 +54,15 @@
## 第 1 步:Coordinator 产出(确认前)
1. 产品文档 → `docs/PRD-<feature>.md` 等(Coordinator R/W)。
2. 任务板 → `docs/ack/tasks.yaml`,每条任务带 `expected` + `verification`,验收写成可观测信号(见 `optimization-method.md` §1)。
3. 项目知识 → 从 `docs/ack/knowledge.yaml` 按 component、path、dependency、version
2. 任务板 → `.pouch/ack/tasks.yaml`,每条任务带 `expected` + `verification`,验收写成可观测信号(可见文本 / API 结果 / 交互结果)。
3. 项目知识 → 从 `.pouch/ack/knowledge.yaml` 按 component、path、dependency、version
和 tag 推荐 `active` 条目,Coordinator 确认后写入固定 revision 的显式
`knowledgeRefs`。candidate 不参与选择。
4. 校验结构与引用:
```bash
python3 <ack-skill-dir>/scripts/validate_tasks.py docs/ack/tasks.yaml
python3 <ack-skill-dir>/scripts/validate_knowledge.py docs/ack/knowledge.yaml --tasks docs/ack/tasks.yaml
python3 <ack-skill-dir>/scripts/validate_tasks.py .pouch/ack/tasks.yaml
python3 <ack-skill-dir>/scripts/validate_knowledge.py .pouch/ack/knowledge.yaml --tasks .pouch/ack/tasks.yaml
```
`project.orchestration` 是 worker profile 的机器 SSOT;未知字段、非 allowlist 模型、
@@ -69,8 +71,8 @@ python3 <ack-skill-dir>/scripts/validate_knowledge.py docs/ack/knowledge.yaml --
读取任务上下文使用:
```bash
python3 <ack-skill-dir>/scripts/select_tasks.py docs/ack/tasks.yaml
python3 <ack-skill-dir>/scripts/select_tasks.py docs/ack/tasks.yaml \
python3 <ack-skill-dir>/scripts/select_tasks.py .pouch/ack/tasks.yaml
python3 <ack-skill-dir>/scripts/select_tasks.py .pouch/ack/tasks.yaml \
--task-id <ack-task-id>
```
@@ -86,10 +88,9 @@ python3 <ack-skill-dir>/scripts/select_tasks.py docs/ack/tasks.yaml \
## 第 2 步:决定 worktree
见 `closed-loop.md` §「子任务放哪」:
- 需求大 / 要并行 / 要保基线分支干净 → 新建隔离 worktree。
- 小改动 / 串行修复 → 当前 worktree 起子 agent。
用户确认后的闭环步骤由 SKILL.md「工作」加载,不要在本步预读其它规范。
---
@@ -99,7 +100,7 @@ python3 <ack-skill-dir>/scripts/select_tasks.py docs/ack/tasks.yaml \
terminal 不能单独授权复用。复用候选必须属于同一轮 ACK、处于空闲状态,且角色、
profile、worktree 与启动身份仍完全匹配;还必须通过受信后端清理历史消息并取得可核对
的新会话身份。当前 Orca 接口缺少该清理证明,所以 Orca 派发仍创建 fresh worker。
原因和边界见 `model-routing.md` §「Receipt、审计与复用边界」。
复用边界以 SKILL.md「启动 worker」为准,不要在本步预读其它规范。
先查看目标 profile hash,确认本次结构化配置。这个 hash 只用于审计和漂移比较,
不能用于匹配或复用旧 receipt / 既有终端:
@@ -146,7 +147,7 @@ worktree 走同一套 `plan` -> 带 expected fingerprint 的 `launch`。在调
`allowedWorktrees`)。profile
只允许 `read-only` 或 `workspace-write`;v0.10 的 full-access 授权通道尚未实现,
任何 bypass、YOLO/force 或关闭 sandbox 的请求都必须失败,不能手写命令兜底。
选型与升级见 `model-routing.md`。
选型与升级以 SKILL.md「启动 worker」为准。
---
@@ -163,7 +164,7 @@ task-create → dispatch 给 DEV → 先确认 DEV 已开始执行(read/probe
→ 三轮有效产品失败:leftover,升级复盘,继续下一个
```
具体命令见 `orca-adapter.md`(Orca)或 `closed-loop.md` §「手动模式」(无 Orca);派发文案见 `prompt-templates.md`。
Orca / 手动命令与派发文案由 SKILL.md「启动 worker」按条件加载。
Coordinator 只内联本轮 `knowledgeRefs` 指向的少量知识,不要求 worker 全量读取
知识库。知识正文不得作为自由 shell 执行;需要命令时只能引用项目已审查的检查
@@ -175,27 +176,15 @@ Coordinator 只内联本轮 `knowledgeRefs` 指向的少量知识,不要求 wo
## 第 5 步:可选交付
用户说「重新布测试环境」时加载 deployer skill 执行
`intents.testEnvironment` 绑定;「发布一个版本」时按 `delivery.md` §3.1 的
release profile 执行。intent 为 null 时先做交付配置维护。
用户说「重新布测试环境」或「发布一个版本」时,转入 SKILL.md 对应模式,不要在本步预读交付规范。intent 为 null 时先做交付配置维护。
所选任务 `verified` 后,按 `regression.md` 把本轮黑盒路径收获进
`docs/ack/regression.yaml`(新增/更新/退役/无回归四选一,用户确认后写入)。
用户说「回归」时先布测试环境,再派 Test 按目录执行。
所选任务 `verified` 后,转入 SKILL.md「运行回归」给出新增/更新/退役/无回归四选一,用户确认后写入 `.pouch/ack/regression.yaml`。用户说「回归」时先布测试环境,再派 Test 按目录执行。
所选任务都由 Coordinator 标记为 `verified` 后,若用户确认的计划包含交付,按
`delivery.md` 执行所选 profile。启用交付时必须在计划中默认列出 `defaultProfile`,
用户可明确取消,Coordinator 不能静默省略。先重新校验 `delivery.yaml`,固定当前 commit 和
config revision,然后按有序步骤调用项目入口与已安装的低层 skill。每一步证据写入
`tasks.yaml.deliveryRuns`;默认 profile 到 `validation_ready` 或 `review_ready` 即停止。
前者必须把测试环境地址和用户下一步交付出来;stable 发布和 production 部署必须在
approval 步骤再次确认。失败时保留任务的 `verified`,把
delivery run 标为 `blocked` 或 `failed`。
所选任务都由 Coordinator 标记为 `verified` 后,若用户确认的计划包含交付,转入 SKILL.md 对应交付模式。启用交付时必须在计划中默认列出 `defaultProfile`,用户可明确取消,Coordinator 不能静默省略。先重新校验 `delivery.yaml`,固定当前 commit 和 config revision,然后按有序步骤调用项目入口与已安装的低层 skill。每一步证据写入 `tasks.yaml.deliveryRuns`;默认 profile 到 `validation_ready` 或 `review_ready` 即停止。前者必须把测试环境地址和用户下一步交付出来;stable 发布和 production 部署必须在 approval 步骤再次确认。失败时保留任务的 `verified`,把 delivery run 标为 `blocked` 或 `failed`。
## 第 6 步:收尾
一轮结束时 Coordinator 必须能回答 `optimization-method.md` §「结束条件」的问题:
哪些 verified、哪些 leftover、各失败几轮、工作树是否干净、还有没有未处理项。
一轮结束时 Coordinator 必须能回答:哪些 verified、哪些 leftover、各失败几轮、工作树是否干净、还有没有未处理项。复验轮次争议以 SKILL.md「边界」为准。
Coordinator 最后标记整轮任务完成后,用 `scripts/reclaim_workers.py` 先 dry-run
审阅决策、再 `--apply` 关闭所有只关联 `verified` 任务的 Developer/Test 终端并核对
回执;receipt 和落盘证据继续保留。仍关联 `blocked`、`failed_retest`、`leftover`
+3 -3
View File
@@ -66,7 +66,7 @@ profile 升级时不得复用旧 worker,Test 也不得使用 Developer 的强
### 机器事实源
worker 路由的机器可读事实只保存在 `docs/ack/tasks.yaml` 的
worker 路由的机器可读事实只保存在 `.pouch/ack/tasks.yaml` 的
`project.orchestration`。`project.md` 可以解释项目为何选某个档位,但不能另写一份
完整启动命令或覆盖机器配置。字段结构以 `templates/tasks.schema.json` 为准。
@@ -135,12 +135,12 @@ python3 <ack-skill-dir>/scripts/launch_worker.py launch \
--expected-launch-fingerprint <plan 中的 sha256:...>
```
`--project-root` 始终指向保存权威 `docs/ack/tasks.yaml` 的项目根;`--worktree` 是本次
`--project-root` 始终指向保存权威 `.pouch/ack/tasks.yaml` 的项目根;`--worktree` 是本次
worker 实际工作的绝对路径,两者可以不同。后者仍必须与项目根属于同一 Git
仓库(v0.19 起由 launcher 按 `git worktree list` 注册表 + 同 common-dir 校验,
不再依赖 `allowedWorktrees` 白名单)。
项目状态文件固定从 `--project-root/docs/ack/` 解析;任务板不需要保存 `repoPath` 或
项目状态文件固定从 `--project-root/.pouch/ack/` 解析;任务板不需要保存 `repoPath` 或
`devWorktree`。旧任务板中的这两个字段仅作兼容信息,launcher 不使用它们授权或定位。
`projectRoot`、任务板内容摘要和 worker worktree identity 都会进入 launch fingerprint,
因此切换权威项目根、任务板内容或 worker 路径后必须重新生成并审阅 plan。
+1 -1
View File
@@ -177,7 +177,7 @@ one dispatch = one bug = one acceptance path
## 8. 优先让测试可执行化
如果某个问题需要多轮修复,说明它值得沉淀成自动化检查。黑盒回归目录是
`docs/ack/regression.yaml`(见 `regression.md`);可执行脚本仍由 Test 维护在
`.pouch/ack/regression.yaml`(见 `regression.md`);可执行脚本仍由 Test 维护在
`<integration_test_paths>`。优先级:
1. 把本轮验收信号收获进 `regression.yaml`。
+3 -3
View File
@@ -58,7 +58,7 @@ Coordinator 用这些模板向 **Developer** 派发修复、向 **Test** 派发
- 不要写 tasks.yaml,不要标记 verified。
- 不要写 knowledge.yaml,不要自行扩展或全量读取知识库;candidate 不是已生效规则。
- 不要把知识正文或 path/args 拼成 shell 命令。只把 verification.ref 交给
`<ack-skill-dir>/scripts/run_verification.py docs/ack/knowledge.yaml
`<ack-skill-dir>/scripts/run_verification.py .pouch/ack/knowledge.yaml
<verification-ref> --project-root <project-root>`。
- 不要提交或推送,除非用户明确要求。
- 最小 diff,只改本任务根因,避免无关重构;若必须先重构请停下说明并请示。
@@ -137,9 +137,9 @@ Developer 本轮声称(仅供参考,不作数):
- 若 worker、权限、服务、测试数据、浏览器或工具导致验收无法完成,明确回报
`environmentFailure`,不要把“未验证”写成产品 `signals-failed`;若已有独立产品失败
证据,则分别列出产品信号与环境限制。
- 需要时把本轮通过的黑盒路径写成 `regressionCandidates`(见 regression.md),不要直接改 `docs/ack/regression.yaml`。
- 需要时把本轮通过的黑盒路径写成 `regressionCandidates`(见 regression.md),不要直接改 `.pouch/ack/regression.yaml`。
- 对每条适用的 `knowledgeRef`,把它的 verification.ref 交给
`<ack-skill-dir>/scripts/run_verification.py docs/ack/knowledge.yaml
`<ack-skill-dir>/scripts/run_verification.py .pouch/ack/knowledge.yaml
<verification-ref> --project-root <project-root>`,并回报 `knowledgeChecks`。
对 candidate 使用独立观测验证,不能复述 Developer 的结论作为证据。
+6 -6
View File
@@ -8,15 +8,15 @@
## 1. 目录
项目状态是 `docs/ack/regression.yaml`。任务板用
`project.regressionFile: docs/ack/regression.yaml` 声明,运行证据写在
项目状态是 `.pouch/ack/regression.yaml`。任务板用
`project.regressionFile: .pouch/ack/regression.yaml` 声明,运行证据写在
`tasks.yaml.regressionRuns`。新项目初始化会生成空目录;旧项目没有该文件时,
「运行回归」先停下来,用户授权后再从模板补齐。
只有 Coordinator 写 `regression.yaml`。Test 在复测报告里提名
`regressionCandidates`;用户确认后 Coordinator 落盘,并把 case id 写入任务的
`regressionRefs`。不要把用例散落到第二份文档,也不要让 Test 直接改
`docs/ack/`。
`.pouch/ack/`。
`tests/browser/**` 仍可由 Test 维护可执行脚本。用例可用可选 `automationRef`
指向那些脚本;没有脚本时 Test worker 按目录里的步骤和验收信号执行。
@@ -30,10 +30,10 @@ Playwright。
读取时用 `scripts/select_regression.py`,不要把完整目录注入上下文:
```bash
python3 <ack-skill-dir>/scripts/select_regression.py docs/ack/regression.yaml
python3 <ack-skill-dir>/scripts/select_regression.py docs/ack/regression.yaml \
python3 <ack-skill-dir>/scripts/select_regression.py .pouch/ack/regression.yaml
python3 <ack-skill-dir>/scripts/select_regression.py .pouch/ack/regression.yaml \
--suite full
python3 <ack-skill-dir>/scripts/select_regression.py docs/ack/regression.yaml \
python3 <ack-skill-dir>/scripts/select_regression.py .pouch/ack/regression.yaml \
--case-id REG-login-001
```
@@ -27,7 +27,7 @@ ACK 默认三个独立 Agent:**Coordinator 只编排、Test 只验证、Develo
上面的表定义了**边界**(谁能碰什么),这一节定义**能力**(每个角色到底该怎么做好自己的事)。每个角色用同一骨架描述:`Outcome`(产出什么)/ `Must Do`(必须做)/ `Must Not`(不能做)/ `Evidence`(拿什么证明)/ `Output`(交付格式)。派发 prompt 会引用这里,见 `prompt-templates.md`。
这些是**通用工程习惯**,不含项目命令与路径;项目差异写在覆盖层文件(默认 `docs/ack/project.md`)。装了外部 skill 的环境可按每个角色末尾的「可选 skills」加速,未装则照本清单执行,不阻塞。
这些是**通用工程习惯**,不含项目命令与路径;项目差异写在覆盖层文件(默认 `.pouch/ack/project.md`)。装了外部 skill 的环境可按每个角色末尾的「可选 skills」加速,未装则照本清单执行,不阻塞。
### Coordinator (PM):拆解与终检
@@ -97,7 +97,7 @@ ACK 默认三个独立 Agent:**Coordinator 只编排、Test 只验证、Develo
## 路径权限模板
目标项目在自己的**覆盖层文件**中填入实际路径(模板见 `templates/project.template.md`;覆盖层默认 `docs/ack/project.md`,路径记在 `tasks.yaml` 的 `project.overlayFile`)。
目标项目在自己的**覆盖层文件**中填入实际路径(模板见 `templates/project.template.md`;覆盖层默认 `.pouch/ack/project.md`,路径记在 `tasks.yaml` 的 `project.overlayFile`)。
| 路径 | Coordinator | Test | Developer | 说明 |
|------|:-----------:|:----:|:---------:|------|
+1 -1
View File
@@ -51,7 +51,7 @@ from worker_profiles import ( # noqa: E402
PROTOCOL_VERSION = LAUNCH_PROTOCOL_VERSION
RECEIPT_VERSION = 1
ENVIRONMENT_POLICY = "per-cli-allowlist-v1"
TASKS_RELATIVE_PATH = Path("docs/ack/tasks.yaml")
TASKS_RELATIVE_PATH = Path(".pouch/ack/tasks.yaml")
MAX_CONTROL_OUTPUT = 1024 * 1024
MAX_RECORD_SIZE = 256 * 1024
LAUNCH_TTL_SECONDS = 120
+3 -3
View File
@@ -57,7 +57,7 @@ def _validate_knowledge_location(
knowledge_path: Path,
project_root: Path,
) -> str | None:
expected = project_root / "docs" / "ack" / "knowledge.yaml"
expected = project_root / ".pouch" / "ack" / "knowledge.yaml"
lexical = Path(os.path.abspath(knowledge_path.expanduser()))
if lexical != expected:
return (
@@ -171,7 +171,7 @@ def load_authoritative_knowledge(
) -> tuple[dict[str, Any] | None, str | None]:
source_fd, open_error = _open_regular_beneath(
project_root,
"docs/ack/knowledge.yaml",
".pouch/ack/knowledge.yaml",
require_executable=False,
)
if open_error is not None or source_fd is None:
@@ -329,7 +329,7 @@ def main(argv: list[str] | None = None) -> int:
parser.add_argument("verification_ref", help="verificationRegistry 中的检查 ID")
parser.add_argument(
"--project-root",
help="项目根目录;默认从 knowledge.yaml 的 docs/ack 布局或 Git 推断",
help="项目根目录;默认从 knowledge.yaml 的 .pouch/ack 布局或 Git 推断",
)
args = parser.parse_args(argv)
+4 -4
View File
@@ -4,9 +4,9 @@
默认返回 smoke 套件。本脚本只输出数据,不执行 steps 或 automationRef。
用法:
python3 select_regression.py docs/ack/regression.yaml
python3 select_regression.py docs/ack/regression.yaml --suite full
python3 select_regression.py docs/ack/regression.yaml --case-id REG-login-001
python3 select_regression.py .pouch/ack/regression.yaml
python3 select_regression.py .pouch/ack/regression.yaml --suite full
python3 select_regression.py .pouch/ack/regression.yaml --case-id REG-login-001
"""
from __future__ import annotations
@@ -65,7 +65,7 @@ def select_cases(
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="选择 ACK 回归用例")
parser.add_argument(
"regression", nargs="?", default="docs/ack/regression.yaml"
"regression", nargs="?", default=".pouch/ack/regression.yaml"
)
parser.add_argument(
"--suite",
+1 -1
View File
@@ -197,7 +197,7 @@ def _parser() -> argparse.ArgumentParser:
parser.add_argument(
"tasks",
nargs="?",
default="docs/ack/tasks.yaml",
default=".pouch/ack/tasks.yaml",
help="任务板路径",
)
parser.add_argument("--task-id", action="append", default=[])
+11 -9
View File
@@ -5,9 +5,9 @@
检查引用、步骤顺序、默认 profile 安全边界、敏感信息和仓库内入口路径。
用法:
python3 validate_delivery.py docs/ack/delivery.yaml
python3 validate_delivery.py docs/ack/delivery.yaml \
--tasks docs/ack/tasks.yaml --project-root <project-root>
python3 validate_delivery.py .pouch/ack/delivery.yaml
python3 validate_delivery.py .pouch/ack/delivery.yaml \
--tasks .pouch/ack/tasks.yaml --project-root <project-root>
退出码: 0 通过 / 1 校验失败 / 2 环境或用法错误。
"""
@@ -707,11 +707,13 @@ def _validate_test_environment_intent(
return
if project_root is None:
return
env_dir = project_root / ".skiff" / "deployer" / env
env_dir = project_root / ".pouch" / "deployer" / env
if not env_dir.is_dir():
env_dir = project_root / ".skiff" / "deployer" / env
if not env_dir.is_dir():
errors.append(
"intents.testEnvironment.env: 找不到 "
f".skiff/deployer/{env};先按 deployer skill 配置项目测试环境"
f".pouch/deployer/{env};先按 deployer skill 配置项目测试环境"
)
@@ -816,8 +818,8 @@ def validate_tasks_link(delivery: dict[str, Any], tasks: dict[str, Any]) -> list
project = tasks.get("project")
if not isinstance(project, dict):
return ["tasks.project 必须是对象"]
if project.get("deliveryFile") != "docs/ack/delivery.yaml":
errors.append("tasks.project.deliveryFile 必须固定为 docs/ack/delivery.yaml")
if project.get("deliveryFile") != ".pouch/ack/delivery.yaml":
errors.append("tasks.project.deliveryFile 必须固定为 .pouch/ack/delivery.yaml")
delivery_project = delivery.get("project")
if (
isinstance(delivery_project, dict)
@@ -845,8 +847,8 @@ def validate_with_schema(data: dict[str, Any], schema_path: Path) -> list[str]:
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="校验 ACK 项目交付契约")
parser.add_argument("delivery", nargs="?", default="docs/ack/delivery.yaml")
parser.add_argument("--tasks", help="关联的 docs/ack/tasks.yaml")
parser.add_argument("delivery", nargs="?", default=".pouch/ack/delivery.yaml")
parser.add_argument("--tasks", help="关联的 .pouch/ack/tasks.yaml")
parser.add_argument("--project-root", help="项目根目录;提供后检查入口路径")
parser.add_argument("--schema", help="delivery.schema.json 路径(默认自动探测)")
args = parser.parse_args(argv)
+7 -7
View File
@@ -189,10 +189,10 @@ def _timestamp(value: Any) -> datetime | None:
def infer_project_root(document_path: Path) -> Path | None:
"""从标准 docs/ack 布局或 Git marker 推断项目根,不解析文档 symlink。"""
"""从标准 .pouch/ack 布局或 Git marker 推断项目根,不解析文档 symlink。"""
lexical = document_path.expanduser().absolute()
parent = lexical.parent
if parent.name == "ack" and parent.parent.name == "docs":
if parent.name == "ack" and parent.parent.name in {".pouch", "docs"}:
return parent.parent.parent.resolve()
for candidate in (parent, *parent.parents):
if (candidate / ".git").exists():
@@ -245,7 +245,7 @@ def _tasks_project_root(
return inferred
# Legacy task boards may still declare repoPath. It is only a fallback for
# non-standard layouts; docs/ack location is authoritative when available.
# non-standard layouts; .pouch/ack location is authoritative when available.
project = tasks_data.get("project")
repo_path = project.get("repoPath") if isinstance(project, dict) else None
if _nonempty(repo_path):
@@ -779,10 +779,10 @@ def _validate_knowledge_file_binding(
knowledge_file = project.get("knowledgeFile")
if not _nonempty(knowledge_file):
return ["[tasks] project.knowledgeFile 必填"]
if knowledge_file != "docs/ack/knowledge.yaml":
if knowledge_file != ".pouch/ack/knowledge.yaml":
errors.append(
"[tasks] project.knowledgeFile 必须固定为 "
"'docs/ack/knowledge.yaml'"
"'.pouch/ack/knowledge.yaml'"
)
relative = Path(knowledge_file)
segments = knowledge_file.replace("\\", "/").split("/")
@@ -812,7 +812,7 @@ def _validate_knowledge_file_binding(
binding_root = project_root or declared_root
if binding_root is None or not binding_root.is_dir():
errors.append(
"[tasks] 无法从 docs/ack 布局确定现有项目根目录;"
"[tasks] 无法从 .pouch/ack 布局确定现有项目根目录;"
"请传入 --project-root"
)
return errors
@@ -1071,7 +1071,7 @@ def main(argv: list[str] | None = None) -> int:
parser.add_argument("--tasks", help="可选 tasks.yaml,用于跨文件引用校验")
parser.add_argument(
"--project-root",
help="可选项目根目录;默认从 tasks.yaml 的 docs/ack 布局推断",
help="可选项目根目录;默认从 tasks.yaml 的 .pouch/ack 布局推断",
)
args = parser.parse_args(argv)
+7 -7
View File
@@ -5,9 +5,9 @@
始终检查用例 ID、验收信号和跨文件引用。本脚本只解析数据,不执行 steps。
用法:
python3 validate_regression.py docs/ack/regression.yaml
python3 validate_regression.py docs/ack/regression.yaml \
--tasks docs/ack/tasks.yaml
python3 validate_regression.py .pouch/ack/regression.yaml
python3 validate_regression.py .pouch/ack/regression.yaml \
--tasks .pouch/ack/tasks.yaml
退出码: 0 通过 / 1 校验失败 / 2 环境或用法错误。
"""
@@ -241,9 +241,9 @@ def validate_tasks_link(
if not isinstance(project, dict):
return ["[tasks] project 必须是对象"]
regression_file = project.get("regressionFile")
if regression_file != "docs/ack/regression.yaml":
if regression_file != ".pouch/ack/regression.yaml":
errors.append(
"[tasks] project.regressionFile 必须固定为 docs/ack/regression.yaml"
"[tasks] project.regressionFile 必须固定为 .pouch/ack/regression.yaml"
)
if not isinstance(tasks.get("regressionRuns"), list):
errors.append("引用 regressionFile 的任务板必须包含 regressionRuns 列表")
@@ -330,9 +330,9 @@ def validate_all(
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="校验 ACK 项目回归目录")
parser.add_argument(
"regression", nargs="?", default="docs/ack/regression.yaml"
"regression", nargs="?", default=".pouch/ack/regression.yaml"
)
parser.add_argument("--tasks", help="关联的 docs/ack/tasks.yaml")
parser.add_argument("--tasks", help="关联的 .pouch/ack/tasks.yaml")
parser.add_argument("--schema", help="regression.schema.json 路径(默认自动探测)")
args = parser.parse_args(argv)
+6 -6
View File
@@ -1009,17 +1009,17 @@ def validate_builtin(data: dict) -> list[str]:
)
if (
"knowledgeFile" in project
and project.get("knowledgeFile") != "docs/ack/knowledge.yaml"
and project.get("knowledgeFile") != ".pouch/ack/knowledge.yaml"
):
errors.append(
"project.knowledgeFile 必须固定为 docs/ack/knowledge.yaml"
"project.knowledgeFile 必须固定为 .pouch/ack/knowledge.yaml"
)
if (
"deliveryFile" in project
and project.get("deliveryFile") != "docs/ack/delivery.yaml"
and project.get("deliveryFile") != ".pouch/ack/delivery.yaml"
):
errors.append(
"project.deliveryFile 必须固定为 docs/ack/delivery.yaml"
"project.deliveryFile 必须固定为 .pouch/ack/delivery.yaml"
)
if "deliveryFile" in project and not isinstance(data.get("deliveryRuns"), list):
errors.append("引用 deliveryFile 的任务板必须包含 deliveryRuns 列表")
@@ -1027,10 +1027,10 @@ def validate_builtin(data: dict) -> list[str]:
errors.append("deliveryRuns 存在时 project.deliveryFile 必须存在")
if (
"regressionFile" in project
and project.get("regressionFile") != "docs/ack/regression.yaml"
and project.get("regressionFile") != ".pouch/ack/regression.yaml"
):
errors.append(
"project.regressionFile 必须固定为 docs/ack/regression.yaml"
"project.regressionFile 必须固定为 .pouch/ack/regression.yaml"
)
if "regressionFile" in project and not isinstance(
data.get("regressionRuns"), list
@@ -13,7 +13,7 @@ import sys
MIGRATION_MESSAGE = (
"ACK v0.10 已停用自由 worker command 校验器;请在 "
"docs/ack/tasks.yaml 的 project.orchestration 中声明 profile,并仅调用 "
".pouch/ack/tasks.yaml 的 project.orchestration 中声明 profile,并仅调用 "
"scripts/launch_worker.py profile-hash|plan|launch。"
)
+2 -2
View File
@@ -1,8 +1,8 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://git.yumee.top/laily/skills/skills/ack/templates/delivery.schema.json",
"$id": "https://git.yumee.top/laily/pouch/skills/ack/templates/delivery.schema.json",
"title": "ACK project delivery contract",
"description": "docs/ack/delivery.yaml 的权威结构;语义规则由 scripts/validate_delivery.py 补充。",
"description": ".pouch/ack/delivery.yaml 的权威结构;语义规则由 scripts/validate_delivery.py 补充。",
"type": "object",
"required": [
"version",
+1 -1
View File
@@ -1,4 +1,4 @@
# 复制为 docs/ack/delivery.yaml。默认关闭;由用户明确配置后再启用。
# 复制为 .pouch/ack/delivery.yaml。默认关闭;由用户明确配置后再启用。
version: 1
updatedAt: "<YYYY-MM-DDTHH:mm:ss+TZ>"
project:
+2 -2
View File
@@ -1,8 +1,8 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://git.yumee.top/laily/skills/skills/ack/templates/knowledge.schema.json",
"$id": "https://git.yumee.top/laily/pouch/skills/ack/templates/knowledge.schema.json",
"title": "ACK project knowledge guardrails",
"description": "docs/ack/knowledge.yaml 的权威结构。知识只描述约束和验证引用,不保存可执行命令。",
"description": ".pouch/ack/knowledge.yaml 的权威结构。知识只描述约束和验证引用,不保存可执行命令。",
"type": "object",
"required": ["version", "updatedAt", "project", "verificationRegistry", "entries"],
"additionalProperties": false,
+1 -1
View File
@@ -1,4 +1,4 @@
# 复制为 docs/ack/knowledge.yaml,替换占位符。结构见 templates/knowledge.schema.json。
# 复制为 .pouch/ack/knowledge.yaml,替换占位符。结构见 templates/knowledge.schema.json。
# Developer/Test 只能在任务证据中提出 candidate;只有 Coordinator 写入这里。
version: 1
updatedAt: "<YYYY-MM-DDTHH:mm:ss+TZ>"
+9 -9
View File
@@ -3,12 +3,12 @@
> 本项目基于 ACK Skill v<ack_version>。通用规范由 `/ack` 从 Skill 自身的
> `references/` 读取;本文件只保存项目差异。
>
> **本文件是「项目覆盖层」,文件名可配置。** 默认放 `docs/ack/project.md`,
> **本文件是「项目覆盖层」,文件名可配置。** 默认放 `.pouch/ack/project.md`,
> 不占用 `AGENTS.md`,避免与团队已有的 `AGENTS.md` 约定冲突。
> 若希望 Agent 自动加载,可由项目维护者自行在 `AGENTS.md` 中引用本文件;ACK
> 不会自动修改 `AGENTS.md`、`CLAUDE.md` 或其它 Agent 指令文件。
> 无论叫什么,都在 `tasks.yaml` 的 `project.overlayFile` 记录实际路径。
> `docs/ack/` 只保存本项目的 `project.md`、`tasks.yaml`、`knowledge.yaml`、默认关闭的
> `.pouch/ack/` 只保存本项目的 `project.md`、`tasks.yaml`、`knowledge.yaml`、默认关闭的
> `delivery.yaml` 与空的 `regression.yaml`,
> 不复制或链接 Skill。
@@ -18,11 +18,11 @@
- 技术栈:`<tech_stack>`
- 运行命令:`<run_command>`
- Base URL:`<base_url>`
- 任务板:`docs/ack/tasks.yaml`
- 项目知识:`docs/ack/knowledge.yaml`
- 交付契约:`docs/ack/delivery.yaml`(默认关闭)
- 回归目录:`docs/ack/regression.yaml`
- 覆盖层文件:`<overlay_file_path>`(默认 `docs/ack/project.md`)
- 任务板:`.pouch/ack/tasks.yaml`
- 项目知识:`.pouch/ack/knowledge.yaml`
- 交付契约:`.pouch/ack/delivery.yaml`(默认关闭)
- 回归目录:`.pouch/ack/regression.yaml`
- 覆盖层文件:`<overlay_file_path>`(默认 `.pouch/ack/project.md`)
## 通用规范(由 ACK Skill 按需读取)
@@ -39,7 +39,7 @@
## Worker 路由
机器可校验的模型、reasoning effort、权限模式、默认 profile、允许 worktree 和启动
receipt 全部以 `docs/ack/tasks.yaml` 的 `project.orchestration` 与顶层
receipt 全部以 `.pouch/ack/tasks.yaml` 的 `project.orchestration` 与顶层
`workerReceipts` 为准。本文件不保存可执行 worker 命令。
默认 profile:
@@ -126,7 +126,7 @@ Skill 的 `scripts/run_verification.py` 执行,不直接拼接 path/args。检
- `delivery.yaml` 默认关闭,只描述能力,不自动授权提交、推送、发布或部署。测试环境
绑定 deployer,发版写在 `intents.release`;用户明确要求重新部署测试环境或发布
版本时才执行对应 intent。常规交付仍在任务 `verified` 且本次 profile 得到确认后
运行。回归目录在 `docs/ack/regression.yaml`,由用户明确要求「回归」时运行。
运行。回归目录在 `.pouch/ack/regression.yaml`,由用户明确要求「回归」时运行。
- 默认交付 profile 最多到 `validation_ready` 或 `review_ready`;stable 发布或 production 部署必须有
approval 步骤并再次获得明确批准。配置变更只影响下一次 run。
- 每个任务最多派发 3 轮,仍不过标记 `leftover` 并继续下一个。
+1 -1
View File
@@ -2,7 +2,7 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://git.yumee.top/laily/skills/skills/ack/templates/regression.schema.json",
"title": "ACK project regression catalog",
"description": "docs/ack/regression.yaml 的权威结构。用例是给 Test worker 的可观测信号说明书,不是可执行 DSL。",
"description": ".pouch/ack/regression.yaml 的权威结构。用例是给 Test worker 的可观测信号说明书,不是可执行 DSL。",
"type": "object",
"required": ["version", "updatedAt", "project", "cases"],
"additionalProperties": false,
@@ -1,4 +1,4 @@
# 复制为 docs/ack/regression.yaml。只有 Coordinator 写入;Test 通过回报提名。
# 复制为 .pouch/ack/regression.yaml。只有 Coordinator 写入;Test 通过回报提名。
# 结构见 templates/regression.schema.json。
version: 1
updatedAt: "<YYYY-MM-DDTHH:mm:ss+TZ>"
+6 -6
View File
@@ -1,6 +1,6 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://git.yumee.top/laily/skills/skills/ack/templates/tasks.schema.json",
"$id": "https://git.yumee.top/laily/pouch/skills/ack/templates/tasks.schema.json",
"title": "ACK task board",
"description": "tasks.yaml 的权威结构。跨语言可用;参考校验实现见 scripts/validate_tasks.py。",
"type": "object",
@@ -45,7 +45,7 @@
"repoPath": {
"type": "string",
"deprecated": true,
"description": "Legacy informational field; ACK derives project files from --project-root/docs/ack"
"description": "Legacy informational field; ACK derives project files from --project-root/.pouch/ack"
},
"baseUrl": {
"type": "string"
@@ -57,21 +57,21 @@
},
"overlayFile": {
"type": "string",
"description": "项目覆盖层文件路径,默认 docs/ack/project.md,可自定义"
"description": "项目覆盖层文件路径,默认 .pouch/ack/project.md,可自定义"
},
"knowledgeFile": {
"type": "string",
"const": "docs/ack/knowledge.yaml",
"const": ".pouch/ack/knowledge.yaml",
"description": "项目知识护栏库的唯一权威路径"
},
"deliveryFile": {
"type": "string",
"const": "docs/ack/delivery.yaml",
"const": ".pouch/ack/delivery.yaml",
"description": "可选项目交付契约的唯一权威路径"
},
"regressionFile": {
"type": "string",
"const": "docs/ack/regression.yaml",
"const": ".pouch/ack/regression.yaml",
"description": "可选项目回归目录的唯一权威路径"
},
"bugIntake": {
+5 -5
View File
@@ -1,4 +1,4 @@
# 复制为 docs/ack/tasks.yaml,替换占位符。结构见 templates/tasks.schema.json。
# 复制为 .pouch/ack/tasks.yaml,替换占位符。结构见 templates/tasks.schema.json。
version: 1
updatedAt: "<YYYY-MM-DDTHH:mm:ss+TZ>"
source: "Coordinator (PM) Agent"
@@ -6,10 +6,10 @@ ackVersion: "<接入时的 ack skill 版本>"
project:
name: "<project_name>"
baseUrl: "<base_url>"
overlayFile: "docs/ack/project.md"
knowledgeFile: "docs/ack/knowledge.yaml"
deliveryFile: "docs/ack/delivery.yaml"
regressionFile: "docs/ack/regression.yaml"
overlayFile: ".pouch/ack/project.md"
knowledgeFile: ".pouch/ack/knowledge.yaml"
deliveryFile: ".pouch/ack/delivery.yaml"
regressionFile: ".pouch/ack/regression.yaml"
# 可选:飞书 Base Bug 收件箱。只保存 profile 名和资源 ID,绝不保存 App Secret。
# bugIntake:
# provider: "feishu-base"
+176 -14
View File
@@ -1,51 +1,213 @@
# builder
按统一契约完成项目的 DEB 包与 Docker 镜像构建和发布。规范本体见
按统一契约完成项目的 DEB 包、Docker 镜像、Android APK / iOS IPA 构建和发布,
或把产物拷到目录 / 容器。规范本体见
[references/contract.md](references/contract.md),`scripts/check.py` 是契约的
可执行校验器。
## 什么时候使用
- "帮我构建这个项目的 DEB / Docker 镜像"
- "把 1.2.3 发布到包仓库 / 镜像仓库"
- "检查这个项目的 Makefile 是否符合 builder 契约"
- "用 builder 初始化这个项目"
- "帮我构建这个项目的 DEB / Docker 镜像 / APK"
- "把 1.2.3 发布到包仓库 / 镜像仓库 / File Browser"
- "检查这个项目的 makefile.builder 是否符合 builder 契约"
- "看看项目现在的发布流程"
只构建不上传时明确说明即可;上传永远需要你显式授权。
## 安装
```bash
pouch add builder # 当前项目
pouch add builder -g # 全局
```
不要 `pouch init builder`。对新项目说「用 builder 初始化」。
## 使用前准备
- Python 3 和 `make`。
- DEB 轨道需要 `dpkg-deb`;Docker 轨道需要 Docker 和 buildx。
- 发布时在项目根 `.env.builder` 填写仓库地址和 token(不要写进 `.env`,也不要进 git)。
- 正式版本号由 [releaser](../releaser/README.md) 打 annotated tag;builder 只读当前 HEAD。
- 脏工作树默认拒绝发布。
## 项目接入契约
1. 用 create-makefile skill 生成或修正 Makefile(目标 `help/build/clean/version`
+ 条件 `deb/docker/push*`,变量 `ARCH/VERSION/DIST_DIR/PROJECT_NAME`)。
2. 运行 `python3 -I -S <builder>/scripts/check.py .` 直到全部 PASS。
3. 在项目根 `.env` 配置发布环境变量:
对新项目说「用 builder 初始化」。Agent 会探测轨道、按
`templates/makefile.builder` 写出项目根 `makefile.builder`,并列出缺的发布配置。
不改用户已有的 `Makefile` 或 `.env`。
1. `makefile.builder` 目标:`help/build/clean/version` + 条件 `deb/docker/push*`,
变量 `ARCHES/ARCH/VERSION/DIST_DIR/PROJECT_NAME`。
`VERSION` 通过 `include <builder>/scripts/version.mk` 从 Git 推导。
多架构清单是 `ARCHES`,见下方「多架构」。
2. 运行 `python3 -I -S <builder>/scripts/check.py . --ready` 直到构建项 PASS。
缺发布键只挡住上传,不挡住构建。
3. 在项目根 `.env.builder` 配置发布环境变量(不要写进 `.env`):
```text
DEB_SERVER_URL=https://deb.example.com
DEB_REPOSITORY=main
DEB_TOKEN=<token> # 只放 .env 或密钥系统,不进 git
DEB_TOKEN=<token> # 只放 .env.builder 或密钥系统,不进 git
DOCKER_REGISTRY=registry.example.com
COPY_DEST=file-server:/srv
COPY_AS=latest.apk
```
4. 日常发布就是两条命令:`make deb && make push-deb`、`make push-docker`。
4. 日常发布就是两条命令:
`make -f makefile.builder deb && make -f makefile.builder push-deb`、
`make -f makefile.builder push-docker`。
写了 `ARCHES=amd64 arm64` 时,这两条会打出并上传两个 DEB;镜像一次推多平台。
## 多架构
项目要发哪些 CPU 架构,写在**已提交的** `makefile.builder` 里,不要写进
`.env.builder`(那是密钥,换机器会丢)。
```makefile
ARCHES ?= amd64 arm64
```
`ARCH` 是「这一次」:默认等于清单第一项。不传 `ARCH=` 时,`deb` / `push-deb`
按清单逐个做完;`push-docker` 把清单映射成 `linux/amd64,linux/arm64`。
只做其中一个:`make -f makefile.builder deb ARCH=arm64`。
合法值只有 `amd64` 和 `arm64`(空格分隔)。不要默认写成双架构:本机编不出
另一边时,只留一项。
```bash
make -s -f makefile.builder arches # 打印清单,例如 amd64 arm64
make -f makefile.builder deb # 打出两个 .deb
make -f makefile.builder push-deb # 两个都上传(需你授权发布)
make -f makefile.builder push-docker # 一次 buildx 推两个平台
```
产物:
```text
dist/<name>_<version>_amd64.deb
dist/<name>_<version>_arm64.deb
```
`build` 配方必须用 `$(ARCH)`,否则两个包内容会一样。初始化时 Agent 按仓库里
已有命令来写(Go 一般是 `GOOS=linux GOARCH=$(ARCH) go build …`),不另做一套
运行时探测脚本。看不出交叉编法就不要写双架构。
`packaging/DEBIAN/control` 的 `Architecture:` 必须随 `$(ARCH)` 变,不要写死
`amd64`。模板会先拷到 `dist/packaging-$(ARCH)` 再改这一行。
本机临时只推一个平台,可以在 `.env.builder` 设 `PLATFORMS=linux/amd64`,这是
覆盖不是清单。项目清单始终以 `ARCHES` 为准。
存量 `makefile.builder` 没有 `ARCHES` 时行为与以前相同(默认 `amd64`,
`ARCH=arm64` 仍合法)。要改成双架构:加上面那一行,确认 `build` 吃 `$(ARCH)`,
再跑 `python3 -I -S <builder>/scripts/check.py . --ready`。
## 版本号
产物版本从 **当前 HEAD 的 Git 祖先** 推导,不调用 releaser。
正式 tag 仍由 releaser 创建;builder 只读取。完整规则见
[contract.md「版本号」](references/contract.md#版本号)。
| 谁 | 做什么 |
| --- | --- |
| releaser | 选定下一个正式 SemVer,打 annotated tag `vX.Y.Z` |
| builder | 读 HEAD:落在稳定 tag 上则打正式产物,否则打测试产物 |
| `make -f makefile.builder version` | 输出一行规范版本(无 `v` 前缀),DEB / Docker 都从它渲染 |
**不要**用 `git tag \| sort -V \| tail -1` 取全仓库最大号,也不要把
`git describe --dirty` 的原始字符串写进 DEB 或镜像 tag。
### 正式(HEAD 恰好是稳定 tag `v1.4.2`)
```text
Git tag v1.4.2
规范版本 1.4.2
DEB foo_1.4.2_amd64.deb Version: 1.4.2
Docker registry/ns/foo:1.4.2
```
稳定 tag 仅匹配 `v<major>.<minor>.<patch>`,不含 `-rc`、`-app-N` 等后缀。
不是 exact-match 就不是正式包。
### 测试(其它任何 commit)
基线 = 祖先上最近的那颗稳定 tag(没有则为 `0.0.0`)。有基线时测试号把
patch 机械 +1,再加构建时间 `YYYYMMDDTHHMM`、清洗后的分支名、相对距离、短 SHA。
这是排序键,不是在选定下一正式 SemVer。测试与正式进**同一个** apt /
Docker 仓库,所以 DEB 必须用 `~` 挂在下一 patch 上:`1.4.2` <
`1.4.3~20260918T1430…` < `1.4.3`。已装上一正式版的机器可以升到 bugfix 测试包;
同号正式版仍盖住测试。
分支 `feat/login-v2`,相对 `v1.4.2` 第 7 个 commit,SHA `abc1234`,
构建时间 `20260918T1430`(带分钟,同一天重建才排得对):
```text
规范 / DEB 1.4.3~20260918T1430.feat-login-v2.7+gabc1234
文件名 foo_1.4.3~20260918T1430.feat-login-v2.7+gabc1234_amd64.deb
Docker registry/ns/foo:1.4.3-20260918T1430.feat-login-v2.7.gabc1234
```
Docker tag 由规范版本映射:`~` → `-`,`+g` → `.g`(Docker 不允许 `~`)。
无参 `apt upgrade` 会从上一正式版升到下一 patch 的测试包,但不会盖住同号
正式包。生产若不能自动吃到测试包,应拆仓库/组件。构建时间取本机当前本地
时间(含分钟);设了 `SOURCE_DATE_EPOCH` 则用该时刻的 UTC 时间。`+g<sha>`
只用于记录 commit、不参与排序:只写到日期时,同一天重复构建的版本只差
`+g<sha>`,Debian 版本比较把哈希当普通文本比,会把新包判成比旧包小。
注意切换格式当天:Debian 把字母排在标点之前,`20260918T1430…` 小于旧格式
的 `20260918.…`,同一天已上传的旧格式包仍会压住新包,需要在仓库里把那些
旧包移除或停用;从第二天起新格式始终更大。
### 分支名清洗
小写;`/`、`_` 改为 `-`;去掉其它非法字符;压缩连续 `-`;过长截断。
detached HEAD 用 `detached`,CI 可注入 `BUILD_BRANCH` / `CI_COMMIT_BRANCH` /
`GITHUB_REF_NAME`。脏工作树不把 `-dirty` 写进版本(发布本身会拒绝)。
## 使用示例
```text
用 builder 检查这个项目的 Makefile 是否符合契约。
用 builder 初始化这个项目。
用 builder 检查这个项目的 makefile.builder 是否符合契约。
用 builder 构建当前版本的 DEB 和镜像,先不要上传。
用 builder 把 dist/example_1.2.3_amd64.deb 发布到项目已配置的测试仓库。
用 builder 把当前 commit 的产物发布到项目已配置的仓库。
用 builder 发布多平台 linux/amd64,linux/arm64 镜像。
用 builder 把当前版本的 amd64 和 arm64 DEB 打出来并发布。
用 builder 打一份测试 APK 并拷到 File Browser。
```
## Agent 会做什么
1. 初始化:探测 DEB / Docker / Android / iOS / copy 轨道,写出 `makefile.builder` 和 `.env.builder` 骨架,按仓库证据填 `ARCHES`(不默认双架构),不改你已有的 `Makefile` 或 `.env`。
2. 检查:只跑 `scripts/check.py --ready`,不改文件。
3. 构建:走 `make -f makefile.builder`;只获构建授权时不走会自动上传的目标。
4. 发布:先校验产物,再上传。缺发布键、未授权、或可能覆盖同版本时停下问你。
Docker 只有你点名镜像时才走。token 不会写进输出、提交或回复。
## 如何判断完成
- 初始化:`check.py --ready` 的构建项 PASS。只缺发布键算部分完成,仍可构建、不能上传。
- 构建:产物路径、规范版本、架构与 `make -f makefile.builder version` 一致。
- 发布:HTTP 成功或远端 digest 对得上;回复里有包名或镜像引用、SHA-256 / digest、源 commit。
- 测试包不会通过无参 `apt upgrade` 盖住同号正式包;会作为下一 patch 候选盖住上一正式包。
## 脚本一览
| 脚本 | 用途 |
|------|------|
| `scripts/check.py` | 校验项目 Makefile 是否符合契约(`--build` 实构核对产物) |
| `scripts/check.py` | 校验契约(`--ready` 含工具链与发布键名,`--build` 实构核对产物) |
| `templates/makefile.builder` | 初始化用的契约文件骨架,拷到项目根 |
| `templates/env.builder` | 初始化用的 `.env.builder` 骨架(注释键,不含值) |
| `scripts/version.sh` | 从 Git 祖先稳定 tag 推导规范版本 / Docker tag |
| `scripts/version.mk` | `makefile.builder` `include`,设置 `VERSION` 与 `IMAGE_TAG` |
| `scripts/upload_deb.sh` | 上传 `.deb` 到 HTTP 包仓库(multipart package/token/repository_name) |
| `scripts/publish_docker.sh` | buildx 构建 + 推送镜像,远端 digest 验证 |
| `scripts/verify_deb.sh` | 核对包元数据、内容与 SHA-256 |
| `scripts/copy_artifact.sh` | 把产物拷到 `COPY_DEST`(本地目录或 `container:/dir`) |
| `templates/makefile.android.builder` | Android APK + copy 轨道骨架 |
| `templates/makefile.ios.builder` | iOS IPA + copy 轨道骨架 |
环境变量契约、脚本解析顺序(`$BUILDER_SKILL_DIR` → `~/.skills/skills/builder/scripts/`)、
环境变量契约、脚本解析顺序(`$BUILDER_SKILL_DIR` → `~/.pouch/skills/builder/scripts/`)、
脏工作树策略等完整规则见 contract.md。
+54 -119
View File
@@ -1,133 +1,68 @@
---
name: builder
description: >-
按统一契约构建并发布项目的 DEB 包与 Docker 镜像:先校验项目 Makefile 是否符合
builder 契约(check.py),再 make 构建产物,经授权后用 skill 自带脚本上传并验证。
触发词:构建 deb、发布 deb、上传 deb、推送 apt 仓库、打 Debian 包、构建镜像、
发布镜像、推送 Docker 镜像、make push、检查 Makefile 是否符合规范。仅分析打包
逻辑或只构建不上传时也可使用;不会在未获授权时执行任何上传。Docker 轨道保持
显式触发:用户点名(builder/publish docker)时才走镜像发布。
初始化或检查 makefile.builder 契约,再构建或发布 deb/镜像/Android APK/iOS IPA,
或把产物拷到目录/容器。触发词:初始化 builder、检查 makefile.builder、构建/发布
deb、推送 apt、构建/发布 Docker 镜像、打 APK/IPA、拷到 File Browser、make push。
未授权不上传。Docker 仅用户点名镜像时才走。
---
# Builder:DEB / Docker 构建发布
# Builder:DEB / Docker / Android / iOS 构建发布
复用项目已有发布约定,安全地完成"校验 → 构建 → 检查 → 授权 → 上传 → 验证"。
**make 管构建,skill 脚本管发布,本 SKILL.md 只留脚本做不了的决策。**
项目状态是根目录 `makefile.builder` 与发布用 `.env.builder`,不要创建
`.pouch/builder/`,不要改用户的 `Makefile` / `makefile` / `.env`。
分工原则:**make 管构建,skill 脚本管发布,本 SKILL.md 只留脚本做不了的决策。**
开始时解析当前 `SKILL.md` 所在目录,记为 `<skill-dir>`。优先
`git rev-parse --show-toplevel` 解析项目根。
## 何时使用
## 选择模式
- 用户要求构建、发布、上传 `.deb` 包或 Docker/OCI 镜像。
- 用户要求检查项目 Makefile 是否符合 builder 契约。
- 用户要求梳理或接通项目现有的 DEB/镜像发布流程。
- 初始化、接入 builder,或还没有 `makefile.builder`:执行「初始化」。
- 检查契约或发布配置:执行「检查」。
- 构建、发布、上传:执行「工作流」。不要静默初始化。
- 不适用:本地安装/卸载 DEB;RPM/语言包;从零设计打包体系(先出方案);普通编码与 Dockerfile 编辑。
- 改本 skill 自身:契约先改 `scripts/check.py`,再同步 [contract.md](references/contract.md) 与 templates;不在生产上传上试脚本。
不适用:本地安装/卸载 DEB;RPM/APK/语言包管理器;从零设计全新打包体系(先出方案);
普通编码与 Dockerfile 编辑。
## 初始化
1. 确认项目根。探测 `makefile.builder`、`.env.builder`、用户 Makefile(只当抄配方的证据,不改)、`Dockerfile`、`debian/`。`.env.builder` 只看键是否存在且非空,不读、不打印值。不要读取用户 `.env`。
2. 判定轨道:有 Dockerfile → docker;有 deb 信号或用户要打 deb → deb;`app.config.ts` / `assembleRelease` / `.apk` → android;`ios/` / `xcodebuild` / `.ipa` → ios;用户要拷到目录或容器 → copy。可多轨道。都不清则问。不要猜测 registry、token、COPY_DEST 或仓库名。
3. 没有 `makefile.builder`:按轨道拷模板。deb/docker 用 `templates/makefile.builder`;仅 android 用 `templates/makefile.android.builder`;仅 ios 用 `templates/makefile.ios.builder`。按轨道删掉未使用的 deb/docker/push* 段,把 `build` 的 TODO 换成仓库里已有的真实编译命令(可从用户 Makefile 抄配方,但不要 `include` 或递归调用它),且配方必须使用 `$(ARCH)`。Android/iOS 的 `ARCHES` 是构建机 ISA(fat APK/IPA 仍只留本机一项)。按 [contract.md](references/contract.md) 填 `ARCHES`:`debian/control` / CI matrix / 用户点名的子集;否则只写本机(`x86_64`→`amd64`,`aarch64`→`arm64`)。不要默认双架构;看不出交叉编法时只留一项并说明。`include` builder 的 `scripts/version.mk`。多产物把 `push` 改成 `push: push-deb push-docker push-copy` 的子集。不要改用户 Makefile。
4. 已有 `makefile.builder`:跑检查;按 FAIL 给出修补说明。不覆盖 `build`。用户明确要求按契约改时再加 `ARCHES`,并确认配方里有 `$(ARCH)`。不要调用 create-makefile(版本规则冲突)。
5. 没有 `.env.builder`:把 `<skill-dir>/templates/env.builder` 拷到项目根(注释键,不含值)。缺发布键时在报告里给出可粘贴示例,并把 `.env.builder` 加入 `.gitignore`,不要提交。
6. 运行 `python3 -I -S <skill-dir>/scripts/check.py <project-dir> --ready`。结构校验通过且当前轨道能构建才称「完成」;只缺发布键是「部分完成」。契约 FAIL 或轨道工具缺失是「阻塞」。除非用户明确要求,不提交、不推送、不上传。
```text
## builder 初始化:完成 | 部分完成 | 阻塞
已具备: …
待配置: 路径 + 字段 + 可粘贴示例 + 缺了会挡住哪步
工具链: make / docker / dpkg-deb(缺则怎么装,不擅自安装)
下一步: 一句话
```
```text
# .env.builder 键名示例
DEB_SERVER_URL=https://deb.example.com
DEB_REPOSITORY=main
DEB_TOKEN=
DOCKER_REGISTRY=registry.example.com
COPY_DEST=file-server:/srv
COPY_AS=latest.apk
```
## 检查
只读。运行 `check.py <project-dir> --ready`,用同一报告格式,标题改为 `## builder 检查:…`。不写 `makefile.builder` / `.env.builder`,不改用户 Makefile 或 `.env`。用户明确要求修复后再转入初始化。
## 工作流
### 0. 校验契约
契约见 [contract.md](references/contract.md)。Docker 的 registry/tag 不明确时再读 [registry.md](references/registry.md)。
```bash
python3 -I -S <skill-dir>/scripts/check.py <project-dir> # 静态检查
python3 -I -S <skill-dir>/scripts/check.py <project-dir> --build # 额外实构 deb 并核对产物
```
任一 FAIL:停下修复(引导用 create-makefile skill 补齐),不要绕过校验继续发布。
完整要求见 [contract.md](references/contract.md)。存量项目未接契约时走第 6 节
fallback;成功交付一次后引导用户迁移到契约。
### 1. 确认发布边界
上传是外部写操作。仅当用户明确要求发布、上传或提交时执行;只要求查看、诊断或构建
则停在相应阶段。
执行上传前确认:
- 目标服务和仓库来自项目配置(`.env`)或用户输入,不猜测生产端点。
- 认证令牌已通过环境变量或密钥系统提供;绝不写入命令输出、文件、提交或回复,
不用 `set -x` 执行含凭据的命令。
- 相同版本是否允许覆盖;无法确认且可能覆盖时,先询问。
- Docker 轨道需要用户已明确指定目标 registry/repository/tag 后才继续。
脏工作树默认拒绝发布;用户明确接受时设置 `ALLOW_UNCOMMITTED=1` 并在汇报中注明
包含的未提交修改。
### 2. 构建
```bash
make build ARCH=<amd64|arm64> VERSION=<version> # 主产物
make deb ARCH=<amd64|arm64> # DEB 项目
```
版本缺省由 make 从 `git describe --tags --always --dirty` 推导。构建目标若会自动
上传而当前仅获构建授权,改用纯构建目标。执行前确认所需工具可用(docker、
dpkg-deb 等)。不得擅自清理宽泛目录;脚本含 `rm -rf` 时先解析确认为受限构建目录。
### 3. 上传前检查
```bash
find $(DIST_DIR) -maxdepth 2 -type f -name '*.deb' -print
<skill-dir>/scripts/verify_deb.sh <exact-package-path.deb> [期望版本] [期望架构]
```
verify_deb.sh 输出元数据、关键内容清单和 SHA-256。匹配到多个包时不凭文件时间猜测,
向用户确认唯一产物。镜像轨道无需单独校验步骤(publish_docker.sh 自带远端 inspect)。
### 4. 发布
优先 `make push[-deb|-docker]`(契约要求的薄包装);直接调用等价:
```bash
DEB_SERVER_URL=… DEB_TOKEN=… DEB_REPOSITORY=… \
<skill-dir>/scripts/upload_deb.sh <exact-package-path.deb>
DOCKER_REGISTRY=… \
<skill-dir>/scripts/publish_docker.sh # env 优先,flag 可覆盖
```
环境变量缺失时脚本会自动向上查找项目 `.env` 加载(shell 显式值优先)。不把 token
作为命令行参数;不把脚本复制进项目。upload_deb.sh 默认请求 `/api/v2/upload/package`
(multipart 字段 `package`/`token`/`repository_name`,接受 200/201),协议不符时设
`DEB_UPLOAD_PATH` 或改用项目专属逻辑。publish_docker.sh 用 buildx 一步完成构建+推送,
多平台只能走它,不能拆进 make。
### 5. 验证与汇报
发布成功不能只依据"curl 已执行"/"push 已执行"。综合检查:
- 上传命令退出码为零,HTTP 状态与响应体明确成功;镜像以 `imagetools inspect`
的远端 digest 为准。
- 若仓库提供查询/索引/下载地址,确认该版本已可见;索引异步时报告
"上传已接受,索引尚待更新",不声称完全可用。
最终回复给出:包名/镜像引用、版本、架构/platform、产物路径与 SHA-256 或远端 digest、
源 commit 与工作区状态、各阶段验证结果、未完成项或覆盖风险。
## 存量项目 fallback(legacy)
从项目根目录查找,不预设文件位置:
```bash
rg -n -i --hidden --glob '!.git' \
'build-deb|upload-deb|publish-deb|dpkg-deb|debuild|curl.*deb|\.deb\b|aptly|reprepro'
```
重点检查 Makefile、CI 配置、`debian/`、构建脚本和发布文档中的入口、变量传递方式、
端点与认证方式。优先复用已有构建入口;上传仍用 builder 脚本。交付后引导迁移到契约
(create-makefile + check.py 通过为准)。
## 修改 builder 自身时
- 上传/发布脚本是 SSOT:通用行为修改落在 `skills/builder/scripts/`,不同步复制到
业务项目。
- 契约变更先改 `scripts/check.py`,再同步 `references/contract.md`。
- 可用 `bash -n` 检查脚本语法;有 ShellCheck 时一并运行。
- 不通过真实生产上传测试脚本,除非用户明确授权并给出测试版本/仓库。
## 完成标准
- 仅分析:入口、调用链、配置来源和风险已被准确说明。
- 仅校验:check.py 结果逐条可解释,修复建议明确。
- 仅构建:产物已生成并通过 verify_deb.sh,未发生上传。
- 发布:构建检查通过,服务端接受上传,仓库可见性已验证或准确标记为待更新。
1. 跑 `check.py <project-dir>`;需要工具链与发布键时加 `--ready`;要实构 deb 时加 `--build`。契约 FAIL 或轨道工具缺失:停下,转入「初始化」,不要绕过校验,不要改用户 Makefile。只缺发布键:允许构建,禁止上传。未接契约的存量项目按 contract.md §6 发现已有入口,上传仍用 builder 脚本;成功交付一次后引导迁到 `makefile.builder`。
2. 仅当用户明确要求发布、上传或提交时才上传。不猜测生产端点;不把 token 写入输出、文件、提交或回复;不用 `set -x` 跑含凭据的命令。可能覆盖同版本时先问。脏工作树默认拒绝发布;用户明确接受时设 `ALLOW_UNCOMMITTED=1` 并注明未提交修改。
3. 构建:用户指定了架构才传 `ARCH=`;否则不要传,让 makefile 按 `ARCHES` 展开(`make -s -f makefile.builder arches`)。`make -f makefile.builder build` 始终单架构;DEB 用 `make -f makefile.builder deb`。版本按契约从 Git 祖先稳定 tag 推导,不要调用 releaser,不要用全仓库最新 tag。仅获构建授权时不要走会自动上传的目标。脚本含 `rm -rf` 时先确认为受限构建目录。
4. DEB 上传前:对每个包 `verify_deb.sh <exact-package-path.deb> <version> <arch>`。多个包时不凭文件时间猜测。镜像轨道由 `publish_docker.sh` 自带远端 inspect。
5. 发布优先 `make -f makefile.builder push[-deb|-docker|-copy]`,或直接调 `upload_deb.sh` / `publish_docker.sh` / `copy_artifact.sh`。未指定 `ARCH=` 时 `push-deb` 上传清单里每一个 `.deb`。拷贝轨道要 `COPY_DEST`(本地目录或 `container:/dir`);`COPY_AS` 给第一份文件额外名字。脚本缺环境变量时加载 `.env.builder`(shell 显式值优先),不读 `.env`。不把 token 当命令行参数;不把脚本复制进项目。多平台镜像只能走 `publish_docker.sh`,不能拆进 make;不要把项目架构清单写进 `.env.builder`。`build` 里禁止 `docker cp`。
6. 发布成功不能只看「curl/push 已执行」。要有退出码、HTTP 成功或远端 digest;索引异步时报告「上传已接受,索引尚待更新」。最终回复给出包名/镜像引用、版本、架构、SHA-256 或 digest、源 commit、工作区状态和未完成项。
+126 -27
View File
@@ -7,6 +7,18 @@ builder 脚本只做发布,不做项目特定的构建逻辑。
分工原则:**make 管构建(项目内、确定性),skill 脚本管发布(跨项目 SSOT),
Agent 只保留授权判断和歧义处理。**
契约文件固定为项目根 `makefile.builder`,调用方式:
```bash
make -f makefile.builder <target>
```
不要把 builder 目标写进用户的 `Makefile` 或 `makefile`。`check.py` 只读
`makefile.builder`。
发布凭据固定为项目根 `.env.builder`。不要把这些键写进用户的 `.env`。脚本不读取
`.env`。
## 1. Make 目标
### 必备目标(所有项目)
@@ -16,37 +28,102 @@ Agent 只保留授权判断和歧义处理。**
| `help` | 分组列出全部目标;首屏含当前版本 |
| `version` | 输出一行版本号,适合脚本消费 |
| `clean` | 只删除明确、受限的构建产物目录 |
| `build` | 编译/打包主产物;尊重 `ARCH`;**不得内含任何上传动作** |
| `build` | 编译/打包主产物;尊重 `ARCH`(单架构原语);**不得内含任何上传动作** |
### 条件目标
| 目标 | 适用 | 要求 |
|------|------|------|
| `deb` | 有 DEB 产物的项目 | 产出唯一 `$(DIST_DIR)/<name>_<version>_<arch>.deb`;只构建不上传 |
| `deb` | 有 DEB 产物的项目 | 产出 `$(DIST_DIR)/<name>_<version>_<arch>.deb`;只构建不上传。未在命令行/环境指定 `ARCH=` 且 `ARCHES` 多于一项时,按清单逐个递归构建;指定了 `ARCH=` 则只打那一个 |
| `docker` | 有镜像的项目 | 构建本地单平台镜像 `linux/$(ARCH)`;**禁止 `--push`、禁止多平台** |
| `push-deb` | 同时有 DEB 和镜像的项目 | 仅调 builder 的 `upload_deb.sh` 上传 `dist/*.deb` |
| `push-docker` | 同时有 DEB 和镜像的项目 | 仅调 builder 的 `publish_docker.sh` |
| `push` | 单一产物类型时必备;双产物项目为聚合 | 依序调用对应 push-* 或直接调脚本;是发布的唯一 make 入口 |
| `android` | 有 Android APK/AAB 的项目 | 可与 `build` 相同。产出 `$(DIST_DIR)/<name>_<version>_<arch>.apk`(或 `.aab`);只构建不上传。`ARCHES` 是构建机 ISA;fat APK 只留本机一项 |
| `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-copy` | 同时有多种发布轨道的项目 | 仅调 builder 的 `copy_artifact.sh` |
| `push` | 单一产物类型时必备;多产物项目为聚合 | 依序调用对应 push-* 或直接调脚本;是发布的唯一 make 入口 |
规则:
1. 项目有 DEB 产物的判据:Makefile 配方引用 `dpkg-deb`/`debuild` 或产出 `.deb`。
1. 项目有 DEB 产物的判据:`makefile.builder` 配方引用 `dpkg-deb`/`debuild` 或产出 `.deb`。
有镜像的判据:项目根存在 `Dockerfile`。
2. 双产物项目必须拆 `push-deb`/`push-docker`,`push` 依序聚合两者;单产物项目一个
`push` 即可。
Android 判据:配方产出 `.apk` / `.aab`。iOS 判据:配方产出 `.ipa`。
拷贝发布判据:配方调用 `copy_artifact.sh`。
2. 多于一种发布轨道必须拆 `push-deb` / `push-docker` / `push-copy`,`push` 依序聚合;
单轨道项目一个 `push` 即可。
3. `docker` 目标只能本地构建。多平台镜像无法拆成"make 构建 + 单独推送"
(`buildx --push` 是一步),因此多平台发布只能走 `publish_docker.sh`。
4. push 类目标必须是薄包装:解析脚本路径后委托,不内联 curl/token/端点。
未指定 `ARCH=` / `PLATFORMS` 时,`publish_docker.sh` 把 `ARCHES` 映射为
`linux/<arch>,…`。
4. push 类目标必须是薄包装:解析脚本路径后委托,不内联 curl/token/端点/`docker cp`。
5. `build` 保持单架构,供 `deb` 循环调用。不要让 `build` 自己遍历 `ARCHES`。
`build` 禁止 `docker cp`、`copy_artifact.sh`、curl/scp。
## 2. 变量
| 变量 | 默认 | 说明 |
|------|------|------|
| `ARCH` | `amd64` | 仅允许 `amd64` \| `arm64`,非法值必须 `$(error)` 报错并提示合法值 |
| `VERSION` | `` (空) | 为空时由 make 从 `git describe --tags --always --dirty` 推导 |
| `DIST_DIR` | `dist` | DEB 产物目录 |
| `ARCHES` | `amd64` | 本项目要发布的架构清单,空格分隔,子集只能是 `amd64` `arm64`。写在 `makefile.builder` 里并提交;不要放进 `.env.builder`。缺省或无 `arches` 目标时视为单架构 `amd64`。不要默认写成双架构 |
| `ARCH` | `$(firstword $(ARCHES))` | 这一次构建的架构。必须落在 `ARCHES` 里;非法值必须 `$(error)` 并提示合法值。命令行/环境传入时只构建/上传这一项 |
| `VERSION` | `` (空) | 为空时按「版本号」节从 Git 祖先稳定 tag 推导;禁止 `sort -V` 取全局最新 |
| `DIST_DIR` | `dist` | 产物目录(deb / apk / ipa) |
| `PROJECT_NAME` | git 仓库名 | 包名/镜像名主体 |
`ARCHES` 是「这个项目发哪些架构」;`ARCH` 是「这一次编哪个」。宇宙集合仍是 `amd64` \| `arm64`,不开放其它 ISA。
`deb` 配方在打包前必须把 `packaging/DEBIAN/control`(或等价文件)的 `Architecture:` 写成当前 `$(ARCH)`,不要写死 `amd64`。推荐先拷到 `$(DIST_DIR)/packaging-$(ARCH)` 再改,避免弄脏源树。
`build` 配方必须使用 `$(ARCH)`(例如 `GOARCH=$(ARCH)`、`--platform=linux/$(ARCH)`)。配方若忽略 `ARCH`,双架构会打出两个文件名不同、内容却都是宿主机架构的包。Builder 不安装 qemu 或交叉编译器;看不出交叉编法时 `ARCHES` 只留本机一项。
### 版本号
`make -f makefile.builder version` 输出一行规范版本(无 `v` 前缀)。DEB 的
`Version` 与文件名直接用它;Docker tag 由它渲染。推导入口是 `scripts/version.sh`
(`makefile.builder` 通过 `scripts/version.mk` 引用);Builder 只读取 Git 状态,不调用
releaser 选定下一正式 SemVer。测试号把祖先基线的 patch 机械 +1 作为排序键
(`v1.4.2` 之后 → `1.4.3~…`),不是在宣布下一正式号就是 1.4.3。正式 tag 由
releaser 事先打好。`version.sh` 不执行 `git fetch`。
推导前 `git fetch --tags`(本地 linked worktree 共享 tags,不必再 fetch 才
能看见其它 worktree 新打的 tag)。基线是 **HEAD 祖先上最近的稳定 tag**,
不是全仓库 `sort -V` 的最大号。稳定 tag 仅 `v<major>.<minor>.<patch>`。
| 判定 | 规范版本 | DEB 文件 | Docker tag |
| --- | --- | --- | --- |
| HEAD exact-match 稳定 tag `v1.4.2` | `1.4.2` | `name_1.4.2_<arch>.deb` | `1.4.2` |
| 其它 commit;祖先最近稳定 tag `v1.4.2` | `1.4.3~<date>.<branch>.<n>+g<sha>` | `name_1.4.3~<date>.<branch>.<n>+g<sha>_<arch>.deb` | `1.4.3-<date>.<branch>.<n>.g<sha>` |
| 祖先中没有稳定 tag | `0.0.0~<date>.<branch>+g<sha>` | 同上替换规范版本 | 同上映射 |
测试与正式进入同一 apt / Docker 仓库。测试 DEB 必须用 `~` 挂在基线的下一
patch 上:`1.4.2` < `1.4.3~…` < `1.4.3`。已装上一正式版的机器可以升到
bugfix 测试包;同号正式版仍盖住测试。不要写成 `1.4.2~…`(小于已发布的
`1.4.2`),不要写成 `1.4.2+<date>` / `1.4.3-beta`(Debian 里 `-beta` 大于
`1.4.3`)。日期必须写在 `~` 之后。Docker tag 不得含 `~`、`/`、`:`,由规范
版本把 `~` → `-`、`+g` → `.g`。
`<date>`:`YYYYMMDDTHHMM`,构建的日期加分钟。加分钟是为了同一天重复构建
也能排序:只写到日期时,同分支同日的版本前缀完全相同,只剩 `+g<sha>` 可
区分,而 Debian 把哈希当普通文本比较,新构建可能排到旧构建下面(
`apt upgrade` 因此看不到最新包)。已设 `SOURCE_DATE_EPOCH` 时用该时刻的
UTC 时间,否则用本机当前时间。正式 `X.Y.Z` 不加日期。切换格式当天注意:
Debian 把字母排在标点之前,`YYYYMMDDT…` 小于旧格式的 `YYYYMMDD.…`,同日
已上传的旧格式包会压住新包,需要把它们在仓库里移除或停用;次日恢复正常。
`<branch>`:`git rev-parse --abbrev-ref HEAD`,detached 时用
`BUILD_BRANCH` / `CI_COMMIT_BRANCH` / `GITHUB_REF_NAME`,再没有则
`detached`。清洗:小写;`/`、`_` → `-`;去掉非 `[a-z0-9-]`;压缩连续
`-`;过长截断(给 base、日期、距离、SHA 留位置;Docker tag 上限 128)。
`<n>` 为基线 tag 到 HEAD 的 commit 数;`<sha>` 为 7 位短哈希。不要把
`--dirty` 写入版本;脏树发布仍走既有门禁。显式 `VERSION=` / `IMAGE_TAG=`
可覆盖推导,但不得把非 exact-match 的 commit 标成正式 `X.Y.Z`。
`makefile.builder` 不要内联 `git describe` 或 `sort -V`,include 本 skill 的
`scripts/version.mk`:
```makefile
include $(HOME)/.pouch/skills/builder/scripts/version.mk
```
## 3. 发布环境变量
### DEB 轨道
@@ -58,18 +135,29 @@ Agent 只保留授权判断和歧义处理。**
| `DEB_REPOSITORY` | 是 | 目标仓库名 |
| `DEB_UPLOAD_PATH` | 否 | 覆盖默认上传路径 `/api/v2/upload/package` |
### 拷贝轨道
| 变量 | 必填 | 说明 |
|---|---|---|
| `COPY_DEST` | 是 | 本地目录,或 Docker 目标 `container:/absolute/dir`(`docker cp`) |
| `COPY_AS` | 否 | 第一份文件的额外 basename,逗号或空格分隔 |
`copy_artifact.sh FILE [FILE ...]` 把已构建文件拷到 `COPY_DEST`,校验目标存在后打印 SHA-256。不读 `.env`。脏工作树默认拒绝,`ALLOW_UNCOMMITTED=1` 放行。
### Docker 轨道
| 变量 | 必填 | 说明 |
|------|------|------|
| `DOCKER_REGISTRY` | 是 | registry 主机,无 scheme |
| `DOCKER_REPOSITORY` | 否 | 默认取 git 仓库名 |
| `IMAGE_TAG` | 否 | 默认 `git describe --tags --always --dirty` |
| `PLATFORMS` | 否 | 默认 `linux/amd64`;多平台如 `linux/amd64,linux/arm64` |
| `IMAGE_TAG` | 否 | 默认由规范版本渲染:正式为 `X.Y.Z`;测试将 `~` 换成 `-`、`+g` 换成 `.g` |
| `PLATFORMS` | 否 | 本机/本轮覆盖。未设置时由 `ARCHES` 映射为 `linux/<arch>,…`;再没有则 `linux/amd64`。不要把项目的架构清单只写在这里(`.env.builder` 不进 git) |
配置来源优先级:shell 已显式设置的值 > 项目根 `.env` > 失败并询问用户。
`.env` 由 builder 脚本自动向上查找并加载(不回显任何值);当前 shell 已设置的值
优先于 `.env`。
`PLATFORMS` 优先级(高 → 低):用户这轮 `--platform` 或已 export 的 `PLATFORMS` → 命令行 `ARCH=` 映射的 `linux/$(ARCH)`(由 `push-docker` 传入)→ `.env.builder` 的 `PLATFORMS` → `makefile.builder` 的 `ARCHES` → `linux/amd64`。
配置来源优先级:shell 已显式设置的值 > 项目根 `.env.builder` > 失败并询问用户。
`.env.builder` 由 builder 脚本加载(不回显任何值);当前 shell 已设置的值优先。
不要读取或改写用户 `.env`。空值视为未配置。不要提交 `.env.builder`。
### 工作区安全
@@ -78,23 +166,34 @@ Agent 只保留授权判断和歧义处理。**
## 4. 脚本解析顺序
push 目标定位 builder 脚本时按以下顺序,命中即用,不做静默兜底:
push 目标与 `version.sh` 定位 builder 脚本时按以下顺序,命中即用,不做静默兜底:
1. `$BUILDER_SKILL_DIR/scripts/`(特殊安装位置)
2. `$HOME/.skills/skills/builder/scripts/`(标准 clone 位)
2. `$HOME/.pouch/skills/builder/scripts/`(标准 clone 位)
两个位置都不可用时必须失败并提示:设置 `BUILDER_SKILL_DIR`,或把 skills 仓库
clone 到 `~/.skills`。
两个位置都不可用时必须失败并提示:设置 `BUILDER_SKILL_DIR`,或把 pouch 仓库
clone 到 `~/.pouch`。
## 5. 校验
`python3 -I -S <builder-scripts>/check.py <project-dir> [--build]` 对本项目逐条检查
上述要求,任一 FAIL 退出码非零,可直接挂 CI。`--build` 额外实构 `make deb` 并核对
产物元数据(默认只静态检查配方)。校验失败时的修复路径:用 create-makefile skill
补齐或修正 Makefile,不要绕过校验器。
上述要求,任一 FAIL 退出码非零,可直接挂 CI。`--build` 额外实构
`make -f makefile.builder deb` 并核对产物元数据(默认只静态检查配方)。`--ready`
额外检查轨道工具链,以及 `.env.builder` / 环境中的发布键名是否存在且非空(不读取、
不打印值;缺键只挡住发布)。校验失败时的修复路径:按 `templates/makefile.builder` 补齐或
修正 `makefile.builder`,再跑 check.py,不要绕过校验器,不要改用户 Makefile,
不要用 create-makefile(版本推导与本契约冲突)。
## 6. 存量项目(legacy fallback)
未接入契约的项目:builder 仍可按发现流程工作——从 `Makefile`、CI 配置、`debian/`
与发布文档中找已有构建/上传入口,优先复用;上传仍使用 builder 脚本。完成一次成功
交付后应引导用户用 create-makefile 把项目迁移到本契约,之后以 check.py 为准。
未接入契约的项目:builder 仍可按发现流程工作——从用户 `Makefile`、CI 配置、`debian/`
与发布文档中找已有构建/上传入口,优先复用;上传仍使用 builder 脚本。从项目根查找,
不预设文件位置:
```bash
rg -n -i --hidden --glob '!.git' \
'build-deb|upload-deb|publish-deb|dpkg-deb|debuild|curl.*deb|\.deb\b|aptly|reprepro'
```
完成一次成功交付后应引导用户按 `templates/makefile.builder` 写入项目根
`makefile.builder`,之后以 check.py 为准。不把契约目标合并进用户 Makefile。
+9 -7
View File
@@ -7,15 +7,15 @@
| Registry | 必须显式确定,例如 `registry.example.com` |
| Repository | 必须包含项目约定的 namespace;缺省取 git 仓库名 |
| Tag | 必须显式确定;优先使用版本号或 Git SHA |
| Platform | 必须显式确定,例如 `linux/amd64` 或 `linux/amd64,linux/arm64` |
| Platform | 用户这轮显式值,否则 `.env.builder` 的 `PLATFORMS`,否则 `makefile.builder` 的 `ARCHES` 映射为 `linux/<arch>,…`,再否则 `linux/amd64` |
| Dockerfile | 默认 `Dockerfile`,不存在或项目另有约定时明确指定 |
| Context | 默认当前项目根目录 |
信息来源优先级:
1. 用户本次请求中明确给出的值。
2. 当前项目的 `.env` 与 `AGENTS.md`、发布文档。
3. Makefile、CI 配置或现有构建脚本中一致且无歧义的配置。
2. 当前项目的 `.env.builder` 与 `AGENTS.md`、发布文档。不要读用户 `.env`。
3. `makefile.builder`、用户 Makefile、CI 配置或现有构建脚本中一致且无歧义的配置。
4. 询问用户。
不要从其他项目、shell history 或无关的本地配置中猜测发布目标。
@@ -34,7 +34,9 @@
## Tag 策略
- release tag(如 `v1.2.3`)默认视为不可变。
- Git SHA tag 应对应当前源 commit。
- `latest`、`stable` 等浮动 tag 只有在用户明确要求时才发布。
- 用户未给 tag 且项目没有唯一明确规则时,必须询问,不要自行选择。
- 未显式给出 `--tag` / `IMAGE_TAG` 时,使用契约「版本号」渲染出的 tag
(正式 `1.4.2`,测试 `1.4.3-<date>.feat-login.7.gabc1234`),不要退回
`git describe`,不要询问后另选一套。
- 正式产物 tag 与 Git tag `vX.Y.Z` 对应,但镜像 tag **不含** `v`;默认视为不可变。
- `latest`、`stable`、裸分支名等浮动 tag 只有在用户明确要求时才额外发布,
不能代替上面那条唯一身份。
+447 -47
View File
@@ -1,14 +1,18 @@
#!/usr/bin/env python3
"""Executable form of the builder contract (references/contract.md).
Checks a project's Makefile against the contract by probing make itself with
dry runs (`make -n`) instead of parsing Makefile text: includes, conditionals,
and variable expansion are resolved by make, so behavior is what gets judged.
Checks a project's makefile.builder against the contract by probing make
itself with dry runs (`make -f makefile.builder -n`) instead of parsing
makefile text: includes, conditionals, and variable expansion are resolved
by make, so behavior is what gets judged. The default Makefile/makefile is
not read.
Usage:
python3 -I -S check.py <project-dir> [--build]
python3 -I -S check.py <project-dir> [--build] [--ready]
Exit codes: 0 = all PASS, 1 = at least one FAIL, 2 = usage/environment error.
`--ready` 额外检查轨道工具链和发布环境变量键名(只看键是否存在,永不打印值)。
无 makefile.builder 时普通模式退出 2;`--ready` 输出结构化 FAIL 并继续工具链/发布键检查。
Change the contract here first, then mirror the change into contract.md.
"""
@@ -17,6 +21,7 @@ from __future__ import annotations
import argparse
import hashlib
import os
import re
import shutil
import subprocess
@@ -27,8 +32,8 @@ ARCH_VALUES = ("amd64", "arm64")
REQUIRED_TARGETS = ("help", "version", "clean", "build")
UPLOAD_TOKENS = (
"curl ", "curl\t", "scp ", "rsync ", "aptly ", "reprepro ",
"docker push", "buildx build --push", "buildx --push", "upload_deb.sh",
"publish_docker.sh",
"docker push", "docker cp", "buildx build --push", "buildx --push",
"upload_deb.sh", "publish_docker.sh", "copy_artifact.sh",
)
SECRET_PATTERNS = (
re.compile(r"(TOKEN|PASSWORD|SECRET|API_KEY|PASSWD)[A-Z_]*\s*[:?]?=\s*['\"]?[^\s$({\"']+", re.IGNORECASE),
@@ -36,7 +41,9 @@ SECRET_PATTERNS = (
)
FLOATING_TAGS = (":latest", ":stable")
DEB_SHAPE = re.compile(r"^[^_\s]+_[^_\s]+_[^_\s]+\.deb$")
VALID_SCRIPT_NAMES = ("upload_deb.sh", "publish_docker.sh")
VALID_SCRIPT_NAMES = ("upload_deb.sh", "publish_docker.sh", "copy_artifact.sh")
BUILDER_MAKEFILE = "makefile.builder"
BUILDER_ENV = ".env.builder"
PASS = "PASS"
FAIL = "FAIL"
@@ -58,16 +65,22 @@ class Report:
self.skips += 0 if self.skips else 1
def run_make(project: Path, *args: str, timeout: int = 60) -> subprocess.CompletedProcess[str]:
def run_make(
project: Path, *args: str, timeout: int = 60, dry_run: bool = True
) -> subprocess.CompletedProcess[str]:
cmd = ["make", "-C", str(project), "-f", BUILDER_MAKEFILE]
if dry_run:
cmd.append("-n")
cmd.extend(args)
return subprocess.run(
["make", "-C", str(project), "-n", *args],
capture_output=True, text=True, timeout=timeout, check=False,
cmd, capture_output=True, text=True, timeout=timeout, check=False
)
def has_no_rule(result: subprocess.CompletedProcess[str]) -> bool:
err = result.stderr.lower()
return result.returncode != 0 and (
"No rule to make target" in result.stderr or "no rule to make target" in result.stderr.lower()
"no rule to make target" in err or "没有规则可制作目标" in result.stderr
)
@@ -87,6 +100,33 @@ def clean_make_output(result: subprocess.CompletedProcess[str]) -> list[str]:
return lines
def parse_dry_echo(result: subprocess.CompletedProcess[str]) -> str:
"""Recover `@echo …` payload from `make -n` output."""
out_lines = [
ln.lstrip()[5:]
for ln in result.stdout.splitlines()
if ln.lstrip().startswith("echo ")
]
return "\n".join(out_lines).strip()
def read_arches(project: Path) -> list[str] | None:
"""Return ARCHES if the `arches` target exists.
None means a legacy makefile without the target. An empty list means the
target exists but printed nothing usable.
"""
result = run_make(project, "arches")
if has_no_rule(result):
return None
if result.returncode != 0:
return []
raw = parse_dry_echo(result)
if not raw:
return []
return raw.split()
def check_required_targets(report: Report, project: Path) -> dict[str, bool]:
present: dict[str, bool] = {}
lines = []
@@ -100,31 +140,78 @@ def check_required_targets(report: Report, project: Path) -> dict[str, bool]:
def check_arch_guard(report: Report, project: Path) -> None:
title = "ARCH / ARCHES 守卫与缺省值"
problems: list[str] = []
bad = run_make(project, "build", "ARCH=loongarch")
guard_ok = bad.returncode != 0 and ("amd64" in bad.stderr or "arm64" in bad.stderr)
guard_ok = bad.returncode != 0 and (
"amd64" in bad.stderr or "arm64" in bad.stderr or "ARCH" in bad.stderr
)
default_ok = run_make(project, "build").returncode == 0
lines = [
f"invalid ARCH rejected: {'yes' if guard_ok else 'NO'}",
f"default ARCH works: {'yes' if default_ok else 'no'}",
]
hint = "" if guard_ok else "\n Hint: add `$(error ARCH must be amd64 or arm64)` guarded by an ifneq filter."
if guard_ok and default_ok:
report.add(PASS, 2, "ARCH 守卫与缺省值", "\n".join(lines + hint.splitlines()))
if not guard_ok:
problems.append(
"Hint: reject unknown ARCH with `$(error)` naming allowed values "
"(amd64/arm64, or the project's ARCHES)."
)
if not default_ok:
problems.append("`make -n build` without ARCH= must succeed")
arches_make = run_make(project, "arches")
if has_no_rule(arches_make):
lines.append("arches target: absent (legacy; implicit ARCHES = default ARCH)")
elif arches_make.returncode != 0:
problems.append(
f"`make -n arches` failed: {arches_make.stderr.strip() or 'unknown error'}"
)
else:
report.add(FAIL, 2, "ARCH 守卫与缺省值", "\n".join(lines) + hint)
raw = parse_dry_echo(arches_make)
arches = raw.split()
lines.append(f"ARCHES: {raw!r}")
if not arches:
problems.append("arches target printed an empty list")
if "," in raw:
problems.append("ARCHES must be space-separated, not comma-separated")
unknown = [a for a in arches if a not in ARCH_VALUES]
if unknown:
problems.append(f"ARCHES entries must be amd64 or arm64: {unknown}")
if len(arches) != len(set(arches)):
problems.append("ARCHES must not contain duplicates")
outsider = next((a for a in ARCH_VALUES if a not in arches), None)
if outsider:
extra = run_make(project, "build", f"ARCH={outsider}")
extra_ok = extra.returncode != 0
lines.append(
f"ARCH={outsider} rejected (not in ARCHES): "
f"{'yes' if extra_ok else 'NO'}"
)
if not extra_ok:
problems.append(
f"ARCH={outsider} must fail when it is not listed in ARCHES"
)
if problems:
report.add(FAIL, 2, title, "\n".join(lines + [""] + problems))
else:
report.add(PASS, 2, title, "\n".join(lines))
def check_version_output(report: Report, project: Path) -> None:
result = run_make(project, "version")
# Dry run: the echoed `@echo <version>` line IS the would-be output.
out_lines = [ln.lstrip()[5:] for ln in result.stdout.splitlines() if ln.lstrip().startswith("echo ")]
out = "\n".join(out_lines).strip()
out = parse_dry_echo(result)
single = len(out.splitlines()) == 1 and out != ""
no_v = single and not out.startswith("v")
detail = f"stdout={out!r}"
if single and not no_v:
detail += "\ncanonical version must not start with 'v'"
report.add(
PASS if single else FAIL,
PASS if no_v else FAIL,
3,
"version 输出一行非空版本号",
f"stdout={out!r}",
"version 输出一行非空规范版本(无 v 前缀)",
detail,
)
@@ -169,6 +256,24 @@ def check_deb_recipe(report: Report, project: Path, built_deb: Path | None) -> N
problems.append(f"dpkg-deb --info failed on {built_deb.name}")
else:
problems.append("dpkg-deb unavailable; metadata not verified (--build)")
arches = read_arches(project)
if arches and len(arches) > 1 and dry.returncode == 0:
blob = dry.stdout + dry.stderr
missing = [
a for a in arches if f"ARCH={a}" not in blob and f"_{a}.deb" not in blob
]
if missing:
problems.append(
"ARCHES has multiple values but `make -n deb` does not fan out to: "
+ ", ".join(missing)
)
pinned = arches[-1]
one = run_make(project, "deb", f"ARCH={pinned}")
one_blob = one.stdout + one.stderr
if f"_{pinned}.deb" not in one_blob and f"ARCH={pinned}" not in one_blob:
problems.append(
f"`make -n deb ARCH={pinned}` does not build that single arch"
)
if problems:
report.add(FAIL, 5, "deb 目标产物形状与纯构建", "\n".join(problems))
else:
@@ -180,6 +285,45 @@ def detect_docker_project(project: Path) -> bool:
return (project / "Dockerfile").exists() or (project / "docker-compose.yaml").exists()
def detect_android_project(recipe_all: str) -> bool:
return ".apk" in recipe_all or ".aab" in recipe_all
def detect_ios_project(recipe_all: str) -> bool:
return ".ipa" in recipe_all
def detect_copy_project(recipe_all: str) -> bool:
return "copy_artifact.sh" in recipe_all
def check_mobile_recipe(
report: Report, project: Path, *, kind: str, number: int, ext: str
) -> None:
target = "build"
dry = run_make(project, "build")
text = chr(10).join(clean_make_output(dry))
problems = []
if dry.returncode != 0:
problems.append(f"`make -n {target}` failed: {dry.stderr.strip() or 'unknown error'}")
else:
if "dist/" not in text and "$(DIST_DIR)" not in text:
problems.append("recipe does not reference dist/ ($(DIST_DIR)) as artifact location")
wanted = (".apk", ".aab") if kind == "android" else (ext,)
if not any(item in text for item in wanted):
problems.append(f"recipe does not mention {' or '.join(wanted)}")
hits = [token for token in UPLOAD_TOKENS if token in text]
if hits:
problems.append("recipe contains upload commands: " + ", ".join(hits))
if "rm -rf /" in text or "rm -rf ~" in text:
problems.append("recipe contains unrestricted rm -rf")
title = f"{kind} 目标产物形状与纯构建"
if problems:
report.add(FAIL, number, title, "\n".join(problems))
else:
report.add(PASS, number, title, f"{target} writes {ext} under dist/")
def check_docker_recipe(report: Report, project: Path) -> None:
dry = run_make(project, "docker")
text = chr(10).join(clean_make_output(dry))
@@ -196,43 +340,85 @@ def check_docker_recipe(report: Report, project: Path) -> None:
SCRIPT_RESOLVE_SNIPPETS = tuple(
f"{prefix}{name}"
for prefix in ("$$BUILDER_SKILL_DIR", "$BUILDER_SKILL_DIR", "$$HOME/.skills/skills/builder/scripts", "$HOME/.skills/skills/builder/scripts", "~/.skills/skills/builder/scripts")
for prefix in (
"$$BUILDER_SKILL_DIR",
"$BUILDER_SKILL_DIR",
"$$HOME/.pouch/skills/builder/scripts",
"$HOME/.pouch/skills/builder/scripts",
"~/.pouch/skills/builder/scripts",
"$$HOME/.skills/skills/builder/scripts",
"$HOME/.skills/skills/builder/scripts",
"~/.skills/skills/builder/scripts",
)
for name in VALID_SCRIPT_NAMES
)
def check_push_delegates(report: Report, project: Path, dual_artifact: bool) -> None:
targets = ("push-deb", "push-docker") if dual_artifact else ("push",)
def check_push_delegates(
report: Report,
project: Path,
*,
deb_project: bool,
docker_project: bool,
copy_project: bool,
) -> None:
tracks = []
if deb_project:
tracks.append("deb")
if docker_project:
tracks.append("docker")
if copy_project:
tracks.append("copy")
multi = len(tracks) > 1
targets = tuple(f"push-{t}" for t in tracks) if multi else ("push",)
missing = []
inline = []
thin = []
builder_names = ("upload_deb.sh", "publish_docker.sh", "copy_artifact.sh")
for target in targets:
dry = run_make(project, target)
if has_no_rule(dry):
missing.append(target)
continue
text = chr(10).join(clean_make_output(dry))
bad_tokens = [token for token in ("curl ", "scp ", "aptly ", "reprepro ") if token in text]
bad_tokens = [
token
for token in ("curl ", "scp ", "aptly ", "reprepro ", "docker cp")
if token in text
]
if bad_tokens:
inline.append(f"{target}: inline upload command ({', '.join(bad_tokens)})")
elif not any(snippet in text for snippet in SCRIPT_RESOLVE_SNIPPETS) \
and "$(BUILDER_SCRIPT)" not in text and "upload_deb.sh" not in text \
and "publish_docker.sh" not in text:
inline.append(f"{target}: does not call a builder script (expected $BUILDER_SKILL_DIR/... or ~/.skills/... path)")
and "$(BUILDER_SCRIPT)" not in text \
and not any(name in text for name in builder_names):
inline.append(f"{target}: does not call a builder script (expected $BUILDER_SKILL_DIR/... or ~/.pouch/... path)")
else:
thin.append(target)
problems = []
if missing:
problems.append("missing targets: " + ", ".join(missing))
problems.extend(inline)
if deb_project:
push_deb_target = "push-deb" if multi else "push"
if push_deb_target not in missing:
arches = read_arches(project)
if arches and len(arches) > 1:
dry = run_make(project, push_deb_target)
if not has_no_rule(dry):
text = chr(10).join(clean_make_output(dry))
absent = [a for a in arches if f"_{a}.deb" not in text]
if absent:
problems.append(
f"ARCHES has multiple values but `make -n {push_deb_target}` "
"does not upload: " + ", ".join(absent)
)
status = PASS if not problems else FAIL
detail = "\n".join(problems) if problems else "thin wrappers: " + ", ".join(thin)
report.add(status, 7, "push 仅委托 builder 脚本(薄包装)", detail)
def check_secrets_and_tags(report: Report, project: Path) -> None:
makefile = project / "Makefile"
included_text = ""
makefile = project / BUILDER_MAKEFILE
problems = []
files = [makefile]
if makefile.exists():
@@ -254,14 +440,132 @@ def check_secrets_and_tags(report: Report, project: Path) -> None:
report.add(FAIL if problems else PASS, 8, "无内联机密、无隐式 latest/stable", "\n".join(problems) or "clean")
def check_script_paths(report: Report) -> None:
import os
DEB_ENV_KEYS = ("DEB_SERVER_URL", "DEB_TOKEN", "DEB_REPOSITORY")
DOCKER_ENV_KEYS = ("DOCKER_REGISTRY",)
COPY_ENV_KEYS = ("COPY_DEST",)
ENV_KEY_LINE = re.compile(r"^([A-Za-z_][A-Za-z0-9_]*)=(.*)$")
def env_file_keys(project: Path) -> set[str]:
"""Return nonempty key names in `.env.builder`. Never return or print values."""
path = project / BUILDER_ENV
keys: set[str] = set()
if not path.is_file():
return keys
try:
text = path.read_text(encoding="utf-8")
except OSError:
return keys
for raw in text.splitlines():
stripped = raw.strip()
if not stripped or stripped.startswith("#"):
continue
match = ENV_KEY_LINE.match(stripped)
if not match:
continue
value = match.group(2).strip().strip("'\"")
if value:
keys.add(match.group(1))
return keys
def key_present(key: str, env_keys: set[str]) -> bool:
return bool(os.environ.get(key)) or key in env_keys
def check_ready_toolchain(
report: Report, *, deb_project: bool, docker_project: bool, copy_project: bool = False
) -> None:
lines = [f"make: {'found' if shutil.which('make') else 'MISSING'}"]
problems = []
if docker_project:
docker_ok = shutil.which("docker") is not None
lines.append(f"docker: {'found' if docker_ok else 'MISSING (blocks docker track)'}")
if not docker_ok:
problems.append("install docker to build/publish images")
else:
lines.append("docker: skipped (no docker track)")
if deb_project:
dpkg_ok = shutil.which("dpkg-deb") is not None
lines.append(
f"dpkg-deb: {'found' if dpkg_ok else 'MISSING (blocks make deb / --build)'}"
)
if not dpkg_ok:
problems.append("install dpkg-dev (or equivalent) to build .deb packages")
else:
lines.append("dpkg-deb: skipped (no deb track)")
if copy_project:
docker_ok = shutil.which("docker") is not None
lines.append(
f"docker (for container COPY_DEST): {'found' if docker_ok else 'optional; needed only for container:/dir dest'}"
)
report.add(
FAIL if problems else PASS,
10,
"轨道工具链",
"\n".join(lines + ([""] + problems if problems else [])),
)
def check_ready_env_keys(
report: Report,
project: Path,
*,
deb_project: bool,
docker_project: bool,
copy_project: bool = False,
) -> None:
env_keys = env_file_keys(project)
lines = []
missing: list[str] = []
if not deb_project and not docker_project and not copy_project:
report.add(SKIP, 11, "发布环境变量键名(不读取值)", "no deb/docker/copy track")
return
if deb_project:
for key in DEB_ENV_KEYS:
found = key_present(key, env_keys)
lines.append(f"{key}: {'present' if found else 'MISSING'}")
if not found:
missing.append(key)
else:
lines.append("DEB_*: skipped (no deb track)")
if docker_project:
for key in DOCKER_ENV_KEYS:
found = key_present(key, env_keys)
lines.append(f"{key}: {'present' if found else 'MISSING'}")
if not found:
missing.append(key)
else:
lines.append("DOCKER_*: skipped (no docker track)")
if copy_project:
for key in COPY_ENV_KEYS:
found = key_present(key, env_keys)
lines.append(f"{key}: {'present' if found else 'MISSING'}")
if not found:
missing.append(key)
else:
lines.append("COPY_*: skipped (no copy track)")
if missing:
lines.extend(
[
"",
f"blocks publish, not build. Put keys in the environment or {BUILDER_ENV}:",
*[f" {key}=" for key in missing],
f"Do not commit {BUILDER_ENV}. Do not put these keys in `.env`. Never print values.",
]
)
report.add(SKIP, 11, "发布环境变量键名(不读取值)", "\n".join(lines))
return
report.add(PASS, 11, "发布环境变量键名(不读取值)", "\n".join(lines))
def check_script_paths(report: Report) -> None:
candidates = []
env_dir = os.environ.get("BUILDER_SKILL_DIR")
if env_dir:
candidates.append(Path(env_dir) / "scripts")
home = Path(os.environ.get("HOME", ""))
candidates.append(home / ".pouch" / "skills" / "builder" / "scripts")
candidates.append(home / ".skills" / "skills" / "builder" / "scripts")
found = next((c for c in candidates if c.is_dir() and any((c / n).is_file() for n in VALID_SCRIPT_NAMES)), None)
if found:
@@ -270,31 +574,85 @@ def check_script_paths(report: Report) -> None:
report.add(FAIL, 9, "builder 脚本路径可达", "\n".join([
"none of these resolve to scripts/upload_deb.sh:",
*(f" {c}" for c in candidates),
"Fix: set BUILDER_SKILL_DIR, or clone the skills repo to ~/.skills.",
"Fix: set BUILDER_SKILL_DIR, or clone the pouch repo to ~/.pouch.",
]))
def build_project(project: Path) -> Path | None:
"""Run `make deb` for real and return the produced .deb, or None."""
result = subprocess.run(["make", "-C", str(project), "deb"], capture_output=True, text=True, timeout=1800, check=False)
"""Run `make -f makefile.builder deb` for real and return the produced .deb, or None."""
result = run_make(project, "deb", timeout=1800, dry_run=False)
if result.returncode != 0:
print(f"--build: `make deb` failed:\n{result.stderr[-2000:]}", file=sys.stderr)
print(
f"--build: `make -f {BUILDER_MAKEFILE} deb` failed:\n{result.stderr[-2000:]}",
file=sys.stderr,
)
return None
debs = sorted((p for p in (project / "dist").glob("*.deb") if p.is_file()), key=lambda p: p.stat().st_mtime, reverse=True)
return debs[0] if debs else None
MAKEFILE_HINT = (
f"Fix: copy <builder-skill>/templates/{BUILDER_MAKEFILE} to the project "
f"root as {BUILDER_MAKEFILE}. Do not put builder targets in Makefile or "
"makefile. Keep help/build/clean/version, include builder "
"scripts/version.mk, and enable deb/docker/push* for the tracks this "
"project actually uses. Then re-run check.py. Do not use create-makefile."
)
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("project", type=Path, help="project directory containing the Makefile")
parser.add_argument("--build", action="store_true", help="actually run `make deb` and verify the artifact")
parser.add_argument(
"project",
type=Path,
help="project directory containing makefile.builder",
)
parser.add_argument(
"--build",
action="store_true",
help="actually run `make -f makefile.builder deb` and verify the artifact",
)
parser.add_argument(
"--ready",
action="store_true",
help="also check toolchain and publish env key names (init/check mode)",
)
args = parser.parse_args(argv)
project = args.project.resolve()
makefile = project / "Makefile"
makefile = project / BUILDER_MAKEFILE
if not makefile.is_file():
print(f"Error: no Makefile in {project}", file=sys.stderr)
return 2
if not args.ready:
print(f"Error: no {BUILDER_MAKEFILE} in {project}", file=sys.stderr)
return 2
report = Report()
report.add(
FAIL,
1,
f"{BUILDER_MAKEFILE} 存在",
f"no {BUILDER_MAKEFILE} in {project}\n{MAKEFILE_HINT}",
)
skip_detail = f"(no {BUILDER_MAKEFILE})"
for number, title, detail in (
(2, "ARCH / ARCHES 守卫与缺省值", skip_detail),
(3, "version 输出一行非空规范版本(无 v 前缀)", skip_detail),
(4, "build 不含上传动作", skip_detail),
(5, "deb 目标产物形状与纯构建", skip_detail),
(6, "docker 目标为本地单平台构建", skip_detail),
(7, "push 仅委托 builder 脚本(薄包装)", skip_detail),
(8, "无内联机密、无隐式 latest/stable", skip_detail),
(9, "builder 脚本路径可达", skip_detail),
):
report.add(SKIP, number, title, detail)
docker_project = detect_docker_project(project)
deb_project = any(project.glob("debian/*"))
check_ready_toolchain(report, deb_project=deb_project, docker_project=docker_project)
check_ready_env_keys(
report, project, deb_project=deb_project, docker_project=docker_project
)
print()
print(f"RESULT: FAILED ({report.failures} check(s) failed)")
return 1
if shutil.which("make") is None:
print("Error: make is required.", file=sys.stderr)
return 2
@@ -303,7 +661,17 @@ def main(argv: list[str] | None = None) -> int:
# Gather every recipe once via dry-running all known targets (best effort).
recipe_all_parts = []
for target in (*REQUIRED_TARGETS, "deb", "docker", "push", "push-deb", "push-docker"):
for target in (
*REQUIRED_TARGETS,
"deb",
"docker",
"android",
"ios",
"push",
"push-deb",
"push-docker",
"push-copy",
):
result = run_make(project, target)
if result.returncode == 0:
recipe_all_parts.append(result.stdout)
@@ -314,19 +682,22 @@ def main(argv: list[str] | None = None) -> int:
built_deb: Path | None = None
deb_project = detect_deb_project(recipe_all, project)
docker_project = detect_docker_project(project)
android_project = detect_android_project(recipe_all)
ios_project = detect_ios_project(recipe_all)
copy_project = detect_copy_project(recipe_all)
if present["build"]:
check_arch_guard(report, project)
check_version_output(report, project)
check_build_has_no_upload(report, project)
else:
report.add(SKIP, 2, "ARCH 守卫与缺省值", "(build target missing)")
report.add(SKIP, 3, "version 输出一行非空版本号", "(version target missing)")
report.add(SKIP, 2, "ARCH / ARCHES 守卫与缺省值", "(build target missing)")
report.add(SKIP, 3, "version 输出一行非空规范版本(无 v 前缀)", "(version target missing)")
report.add(SKIP, 4, "build 不含上传动作", "(build target missing)")
if deb_project:
if args.build:
print("--build: running `make deb` ...")
print(f"--build: running `make -f {BUILDER_MAKEFILE} deb` ...")
built_deb = build_project(project)
if built_deb is None:
print("--build: no .deb produced; artifact checks degrade to recipe-only.", file=sys.stderr)
@@ -339,10 +710,39 @@ def main(argv: list[str] | None = None) -> int:
else:
report.add(SKIP, 6, "docker 目标为本地单平台构建", "(no Dockerfile)")
dual = deb_project and docker_project
check_push_delegates(report, project, dual)
if android_project:
check_mobile_recipe(report, project, kind="android", number=12, ext=".apk")
else:
report.add(SKIP, 12, "android 目标产物形状与纯构建", "(not an Android project)")
if ios_project:
check_mobile_recipe(report, project, kind="ios", number=13, ext=".ipa")
else:
report.add(SKIP, 13, "ios 目标产物形状与纯构建", "(not an iOS project)")
check_push_delegates(
report,
project,
deb_project=deb_project,
docker_project=docker_project,
copy_project=copy_project,
)
check_secrets_and_tags(report, project)
check_script_paths(report)
if args.ready:
check_ready_toolchain(
report,
deb_project=deb_project,
docker_project=docker_project,
copy_project=copy_project,
)
check_ready_env_keys(
report,
project,
deb_project=deb_project,
docker_project=docker_project,
copy_project=copy_project,
)
total_fail = report.failures
print()
+152
View File
@@ -0,0 +1,152 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage:
COPY_DEST=/path/or/container:/dir \
copy_artifact.sh FILE [FILE ...]
Copy built artifacts to COPY_DEST. Dest is a local directory, or a Docker
copy target `container:/absolute/dir` (docker cp). Does not read `.env`.
Environment (project root .env.builder; shell values win):
COPY_DEST Required. Local dir or container:/dir
COPY_AS Optional. Extra basenames for the first file
(comma or space separated)
ALLOW_UNCOMMITTED=1 Publish despite a dirty working tree
Options:
-d DEST Override COPY_DEST
-a NAMES Override COPY_AS
--dry-run Print the copies without executing
-h Show help
The working tree must be clean to publish; set ALLOW_UNCOMMITTED=1 to override.
EOF
}
project_root=$(git rev-parse --show-toplevel 2>/dev/null || true)
if [[ -n "$project_root" && -f "$project_root/.env.builder" ]]; then
while IFS='=' read -r key value; do
key=${key%%[[:space:]]*}
[[ -z "$key" || "$key" == \#* ]] && continue
if [[ -n "${!key:-}" ]]; then
continue
fi
value=${value%\"}; value=${value#\"}; value=${value%\'}; value=${value#\'}
[[ -z "$value" ]] && continue
printf -v "$key" '%s' "$value"
export "$key"
done < <(grep -v '^[[:space:]]*$' "$project_root/.env.builder")
fi
dest=${COPY_DEST:-}
extra_as=${COPY_AS:-}
dry_run=false
while (($#)); do
case "$1" in
-d) dest=$2; shift 2 ;;
-a) extra_as=$2; shift 2 ;;
--dry-run) dry_run=true; shift ;;
-h|--help) usage; exit 0 ;;
--) shift; break ;;
-*) echo "Error: unknown option: $1" >&2; usage >&2; exit 2 ;;
*) break ;;
esac
done
if [[ -z "$dest" || $# -eq 0 ]]; then
echo "Error: COPY_DEST and at least one file are required." >&2
echo "Set them in the environment or the project root .env.builder." >&2
usage >&2
exit 2
fi
if [[ -n "$project_root" ]] && git -C "$project_root" rev-parse HEAD >/dev/null 2>&1; then
if [[ ${ALLOW_UNCOMMITTED:-0} != 1 ]] && ! git -C "$project_root" diff-index --quiet HEAD -- 2>/dev/null; then
echo "Error: working tree has uncommitted changes; refusing to publish." >&2
echo "Commit first, or set ALLOW_UNCOMMITTED=1 to publish anyway." >&2
exit 3
fi
fi
is_docker_dest() {
local d=$1
[[ "$d" != /* && "$d" != ./* && "$d" != ../* && "$d" != ~* ]] \
&& [[ "$d" != *://* ]] \
&& [[ "$d" =~ ^[A-Za-z0-9][A-Za-z0-9_.-]*:/ ]]
}
docker_container=
docker_dir=
local_dir=
if is_docker_dest "$dest"; then
docker_container=${dest%%:*}
docker_dir=${dest#*:}
if [[ -z "$docker_container" || -z "$docker_dir" || "$docker_dir" != /* ]]; then
echo "Error: docker dest must be container:/absolute/dir" >&2
exit 2
fi
docker_dir=${docker_dir%/}
else
local_dir=${dest%/}
fi
split_as() {
local raw=$1
raw=${raw//,/ }
# shellcheck disable=SC2086
printf '%s\n' $raw
}
copy_one() {
local src=$1 name=$2
if [[ ! -f "$src" ]]; then
echo "Error: not a file: $src" >&2
exit 2
fi
if [[ -n "$docker_container" ]]; then
local target="${docker_container}:${docker_dir}/${name}"
if $dry_run; then
echo "docker cp $src $target"
return
fi
docker cp "$src" "$target"
docker exec "$docker_container" ls -l "${docker_dir}/${name}" >/dev/null
echo "copied $(basename -- "$src") -> $target"
else
local target="${local_dir}/${name}"
if $dry_run; then
echo "cp $src $target"
return
fi
mkdir -p "$local_dir"
cp -a "$src" "$target"
test -f "$target"
echo "copied $(basename -- "$src") -> $target"
fi
}
first=true
success=0
for src in "$@"; do
copy_one "$src" "$(basename -- "$src")"
if $first; then
first=false
if [[ -n "$extra_as" ]]; then
while IFS= read -r alias; do
[[ -z "$alias" ]] && continue
copy_one "$src" "$alias"
done < <(split_as "$extra_as")
fi
fi
if ! $dry_run && command -v sha256sum >/dev/null 2>&1; then
sha256sum -- "$src"
fi
success=$((success + 1))
done
echo "copy_artifact: $success file(s) to $dest"
+56 -13
View File
@@ -6,8 +6,8 @@ usage() {
}
# Build and publish a Docker image with buildx. Configuration comes from the
# environment first (optionally loaded from the project root .env); flags
# override.
# environment first (optionally loaded from the project root .env.builder);
# flags override. Does not read `.env`.
#
# Usage:
# publish_docker.sh [--registry HOST] [--repository PATH] [--tag TAG] \
@@ -16,8 +16,9 @@ usage() {
# Environment:
# DOCKER_REGISTRY Required (or --registry)
# DOCKER_REPOSITORY Optional, default: git repository name (or --repository)
# IMAGE_TAG Optional, default: git describe --tags --always --dirty (or --tag)
# PLATFORMS Optional, default: linux/amd64 (or --platform)
# IMAGE_TAG Optional, default: version.sh --docker (or --tag)
# PLATFORMS Optional. Else makefile.builder ARCHES mapped to
# linux/<arch>,… ; else linux/amd64 (or --platform)
# DOCKER_DOCKERFILE Optional, default: Dockerfile (--file)
# DOCKER_CONTEXT Optional, default: . (--context)
# DOCKER_BUILDER Optional buildx builder name (--builder)
@@ -30,18 +31,19 @@ usage() {
project_root=$(git rev-parse --show-toplevel 2>/dev/null || true)
# Load project .env without printing values; explicitly exported shell values keep precedence.
if [[ -n "$project_root" && -f "$project_root/.env" ]]; then
# Load .env.builder without printing values; shell values win. Do not read `.env`.
if [[ -n "$project_root" && -f "$project_root/.env.builder" ]]; then
while IFS='=' read -r key value; do
key=${key%%[[:space:]]*}
[[ -z "$key" || "$key" == \#* ]] && continue
if [[ -n "${!key:-}" ]]; then
continue # shell value already set: wins over .env
continue # shell value already set: wins over .env.builder
fi
value=${value%\"}; value=${value#\"}; value=${value%\'}; value=${value#\'}
[[ -z "$value" ]] && continue
printf -v "$key" '%s' "$value"
export "$key"
done < <(grep -v '^[[:space:]]*$' "$project_root/.env")
done < <(grep -v '^[[:space:]]*$' "$project_root/.env.builder")
fi
git_repo_name=
@@ -52,7 +54,7 @@ fi
registry=${DOCKER_REGISTRY:-}
repository=${DOCKER_REPOSITORY:-$git_repo_name}
tag=${IMAGE_TAG:-}
platform=${PLATFORMS:-linux/amd64}
platform=${PLATFORMS:-}
dockerfile=${DOCKER_DOCKERFILE:-Dockerfile}
build_context=${DOCKER_CONTEXT:-.}
builder=${DOCKER_BUILDER:-}
@@ -81,7 +83,7 @@ if [[ -n "$registry" && ( "$registry" == *://* || "$registry" == */* ) ]]; then
fi
if [[ -z "$registry" ]]; then
echo "Error: DOCKER_REGISTRY (or --registry) is required." >&2
echo "Set it in the environment or the project root .env." >&2
echo "Set it in the environment or the project root .env.builder." >&2
usage >&2
exit 2
fi
@@ -89,24 +91,65 @@ if [[ -z "$repository" || "$repository" == /* || "$repository" == */ || "$reposi
echo "Error: repository must be namespace/name without leading or trailing slash: $repository" >&2
exit 2
fi
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
version_sh=$script_dir/version.sh
if [[ -z "$tag" ]]; then
if [[ -n "$project_root" ]]; then
tag=$(git -C "$project_root" describe --tags --always --dirty 2>/dev/null) || tag=
if [[ ! -x "$version_sh" ]]; then
echo "Error: version.sh not found next to publish_docker.sh: $version_sh" >&2
exit 2
fi
if [[ -z "$tag" ]]; then
if [[ -z "$project_root" ]]; then
echo "Error: IMAGE_TAG (or --tag) is required outside a git repository." >&2
exit 2
fi
tag=$("$version_sh" -C "$project_root" --docker) || {
echo "Error: failed to derive IMAGE_TAG from Git ancestry." >&2
exit 2
}
fi
if [[ "$tag" == *:* || "$tag" == */* ]]; then
echo "Error: tag must not contain : or /: $tag" >&2
exit 2
fi
# Official-shaped tags (X.Y.Z or vX.Y.Z) are only legal on that exact Git tag.
if [[ "$tag" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+$ && -x "$version_sh" && -n "$project_root" ]]; then
derived=$("$version_sh" -C "$project_root" --docker) || true
expected=${tag#v}
if [[ "$derived" != "$expected" ]]; then
echo "Error: IMAGE_TAG $tag looks official but HEAD is $derived" >&2
echo "Official X.Y.Z is allowed only when HEAD exact-matches vX.Y.Z." >&2
exit 2
fi
fi
if [[ "$tag" == latest && ${ALLOW_LATEST:-0} != 1 && "$mode" == push ]]; then
echo "Error: refusing to publish floating tag 'latest'; pass an explicit version." >&2
echo "Set ALLOW_LATEST=1 only when the user explicitly asked for 'latest'." >&2
exit 3
fi
if [[ -z "$platform" ]]; then
if [[ -n "$project_root" && -f "$project_root/makefile.builder" ]]; then
arches=$(make -C "$project_root" -f makefile.builder -s --no-print-directory arches 2>/dev/null || true)
mapped=
for arch in $arches; do
case "$arch" in
amd64|arm64)
if [[ -n "$mapped" ]]; then
mapped="$mapped,linux/$arch"
else
mapped="linux/$arch"
fi
;;
*)
echo "Error: unsupported ARCHES entry from makefile.builder: $arch" >&2
exit 2
;;
esac
done
platform=$mapped
fi
platform=${platform:-linux/amd64}
fi
if [[ "$mode" == load && "$platform" == *,* ]]; then
echo "Error: --load cannot be combined with multiple platforms: $platform" >&2
exit 2
+72
View File
@@ -0,0 +1,72 @@
#!/usr/bin/env bash
# Repro: 755 maintainer scripts must pass; 644 must fail as not executable.
set -euo pipefail
root=$(cd "$(dirname "$0")" && pwd)
verify=$root/verify_deb.sh
scratch=$(mktemp -d)
trap 'rm -rf -- "$scratch"' EXIT
make_deb_755() {
local dest=$1
local pkg=$scratch/pkg-755
rm -rf "$pkg"
mkdir -p "$pkg/DEBIAN" "$pkg/usr/bin"
printf '#!/bin/sh\nexit 0\n' >"$pkg/usr/bin/noop"
chmod 755 "$pkg/usr/bin/noop"
cat >"$pkg/DEBIAN/control" <<'EOF'
Package: verify-deb-fixture
Version: 0.0.1
Architecture: amd64
Maintainer: test <test@example.com>
Description: fixture for verify_deb.sh permission check
EOF
printf '#!/bin/sh\nexit 0\n' >"$pkg/DEBIAN/postinst"
chmod 755 "$pkg/DEBIAN/postinst"
dpkg-deb --build "$pkg" "$dest" >/dev/null
}
# dpkg-deb --build rejects 644 maintainer scripts; strip the execute bit in the
# control tarball so verify_deb.sh still sees a real packaged 644 postinst.
make_deb_644_from() {
local src=$1 dest=$2
local work=$scratch/mutate
rm -rf "$work"
mkdir -p "$work/control"
cp "$src" "$dest"
( cd "$work" && ar x "$dest" )
zstd -d -c "$work/control.tar.zst" | tar -C "$work/control" -xf -
chmod 644 "$work/control/postinst"
tar -C "$work/control" -cf - . | zstd -c >"$work/control.tar.zst"
( cd "$work" && ar r "$dest" control.tar.zst )
}
ok_deb=$scratch/verify-deb-fixture_0.0.1_amd64.deb
bad_deb=$scratch/verify-deb-fixture-noexec_0.0.1_amd64.deb
make_deb_755 "$ok_deb"
make_deb_644_from "$ok_deb" "$bad_deb"
ok_out=$scratch/ok.out
bad_out=$scratch/bad.out
set +e
"$verify" "$ok_deb" 0.0.1 amd64 >"$ok_out" 2>&1
ok_rc=$?
"$verify" "$bad_deb" 0.0.1 amd64 >"$bad_out" 2>&1
bad_rc=$?
set -e
fail=0
if [[ $ok_rc -ne 0 ]] || ! grep -q 'VERIFY: OK' "$ok_out"; then
echo "FAIL: 755 postinst should be treated as executable" >&2
cat "$ok_out" >&2
fail=1
fi
if [[ $bad_rc -eq 0 ]] || ! grep -q 'not executable' "$bad_out"; then
echo "FAIL: 644 postinst should fail as not executable" >&2
cat "$bad_out" >&2
fail=1
fi
if ((fail)); then
exit 1
fi
echo "test_verify_deb.sh: OK"
+10 -9
View File
@@ -15,9 +15,9 @@ Options:
-p UPLOAD_PATH Override DEB_UPLOAD_PATH (default: /api/v2/upload/package)
-h Show help
Environment variables may live in the project root .env; this script walks up
from the current directory, loads it silently (existing shell values win), and
never echoes variable values. The endpoint must accept multipart fields named
Environment variables may live in the project root .env.builder; this script
loads it silently (existing shell values win), never echoes values, and does
not read `.env`. The endpoint must accept multipart fields named
package, token, and repository_name. Authentication is read only from
DEB_TOKEN so it is not exposed in the process command line.
@@ -25,21 +25,22 @@ The working tree must be clean to publish; set ALLOW_UNCOMMITTED=1 to override.
EOF
}
# Locate project root (.git) upward from cwd for .env loading and git checks.
# Locate project root (.git) upward from cwd for .env.builder loading and git checks.
project_root=$(git rev-parse --show-toplevel 2>/dev/null || true)
# Load project .env without printing values; explicitly exported shell values keep precedence.
if [[ -n "$project_root" && -f "$project_root/.env" ]]; then
# Load .env.builder without printing values; shell values win. Do not read `.env`.
if [[ -n "$project_root" && -f "$project_root/.env.builder" ]]; then
while IFS='=' read -r key value; do
key=${key%%[[:space:]]*}
[[ -z "$key" || "$key" == \#* ]] && continue
if [[ -n "${!key:-}" ]]; then
continue # shell value already set: wins over .env
continue # shell value already set: wins over .env.builder
fi
value=${value%\"}; value=${value#\"}; value=${value%\'}; value=${value#\'}
[[ -z "$value" ]] && continue
printf -v "$key" '%s' "$value"
export "$key"
done < <(grep -v '^[[:space:]]*$' "$project_root/.env")
done < <(grep -v '^[[:space:]]*$' "$project_root/.env.builder")
fi
server_url=${DEB_SERVER_URL:-}
@@ -61,7 +62,7 @@ shift $((OPTIND - 1))
if [[ -z "$server_url" || -z "$repository" || -z "$token" || $# -eq 0 ]]; then
echo "Error: DEB_SERVER_URL, DEB_TOKEN, DEB_REPOSITORY, and at least one file are required." >&2
echo "Set them in the environment or the project root .env." >&2
echo "Set them in the environment or the project root .env.builder." >&2
usage >&2
exit 2
fi
+4 -2
View File
@@ -87,8 +87,10 @@ if dpkg-deb --control "$package" "$control_dir" 2>/dev/null; then
for script in preinst postinst prerm postrm; do
if [[ -f "$control_dir/$script" ]]; then
found_scripts=true
mode=$(stat -c '%a' "$control_dir/$script")
if [[ $mode =~ .*[2367]$ ]]; then
# Use -x: octal last digit [2367] is "other write", so 755 (r-x) was
# reported as not executable. 644 still fails because it is not -x.
mode=$(stat -c '%a' "$control_dir/$script" 2>/dev/null || stat -f '%OLp' "$control_dir/$script")
if [[ -x "$control_dir/$script" ]]; then
echo "OK: $script mode $mode"
else
echo "FAIL: $script not executable (mode $mode)" >&2
+42
View File
@@ -0,0 +1,42 @@
# Include from makefile.builder. Sets VERSION (canonical, no leading v)
# and IMAGE_TAG (Docker rendering) via builder version.sh.
#
# Command-line / environment VERSION= is passed as --version (official X.Y.Z
# only when HEAD exact-matches that tag). Unset or empty VERSION is derived.
#
# include $(HOME)/.pouch/skills/builder/scripts/version.mk
_builder_scripts_dir := $(dir $(lastword $(MAKEFILE_LIST)))
ifeq ($(BUILDER_VERSION_SH),)
BUILDER_VERSION_SH := $(wildcard $(_builder_scripts_dir)version.sh)
endif
ifeq ($(BUILDER_VERSION_SH),)
ifneq ($(BUILDER_SKILL_DIR),)
BUILDER_VERSION_SH := $(wildcard $(BUILDER_SKILL_DIR)/scripts/version.sh)
endif
endif
ifeq ($(BUILDER_VERSION_SH),)
BUILDER_VERSION_SH := $(wildcard $(HOME)/.pouch/skills/builder/scripts/version.sh)
endif
ifeq ($(BUILDER_VERSION_SH),)
BUILDER_VERSION_SH := $(wildcard $(HOME)/.skills/skills/builder/scripts/version.sh)
endif
ifeq ($(BUILDER_VERSION_SH),)
$(error version.sh not found; set BUILDER_SKILL_DIR or clone pouch to ~/.pouch)
endif
ifeq ($(filter command line environment,$(origin VERSION)),)
VERSION := $(shell "$(BUILDER_VERSION_SH)")
else ifeq ($(strip $(VERSION)),)
VERSION := $(shell "$(BUILDER_VERSION_SH)")
else
VERSION := $(shell "$(BUILDER_VERSION_SH)" --version "$(VERSION)")
endif
ifeq ($(strip $(VERSION)),)
$(error version.sh produced an empty version)
endif
ifeq ($(origin IMAGE_TAG),undefined)
IMAGE_TAG := $(shell "$(BUILDER_VERSION_SH)" --docker --version "$(VERSION)")
endif
+229
View File
@@ -0,0 +1,229 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage:
version.sh [-C GIT_DIR] [--docker] [--version VER]
Print one line: the canonical artifact version (Debian Version, no leading v).
With --docker, print the Docker tag rendering of that version.
Derivation (builder contract «版本号»):
official HEAD exact-match of vX.Y.Z → X.Y.Z
test nearest ancestor stable tag vX.Y.Z → X.Y.(Z+1)~YYYYMMDDTHHMM.branch.n+gSHA
no tag no stable tag reachable from HEAD → 0.0.0~YYYYMMDDTHHMM.branch+gSHA
YYYYMMDDTHHMM is the build minute (day plus time of day). SOURCE_DATE_EPOCH,
if set, is that instant in UTC; otherwise the local date and time. Official
X.Y.Z never gets a date. Minute precision is required for apt: Debian version
comparison ignores upload order, so same-day rebuilds that differ only in
+gSHA compare as arbitrary text (a newer build can sort below an older one).
Switching from the date-only scheme: Debian orders letters below punctuation,
so 'YYYYMMDDT…' compares *below* the old 'YYYYMMDD.…' form. Packages built on
the switch day therefore rank under that day's date-only packages until those
are removed or disabled in the repository; from the next day on, the new form
always ranks higher.
Stable tags match v<major>.<minor>.<patch> only. Baseline is ancestry, not
the highest version in the repository. This script does not fetch tags.
--version VER overrides derivation. Official-shaped VER (X.Y.Z, optional
leading v) is accepted only when HEAD exact-matches that tag. Test-shaped
VER is used as-is.
Branch name: git symbolic-ref, or BUILD_BRANCH / CI_COMMIT_BRANCH /
GITHUB_REF_NAME when detached, else "detached". Sanitized to [a-z0-9-],
max 32 characters.
EOF
}
root=.
mode=canonical
override=
while (($#)); do
case "$1" in
-C) root=$2; shift 2 ;;
--docker) mode=docker; shift ;;
--version) override=$2; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) echo "Error: unknown argument: $1" >&2; usage >&2; exit 2 ;;
esac
done
if ! git -C "$root" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
echo "Error: not a git repository: $root" >&2
exit 2
fi
root=$(git -C "$root" rev-parse --show-toplevel)
gitc() {
git -C "$root" "$@"
}
is_stable_tag() {
[[ "$1" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]
}
is_official_version() {
[[ "$1" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]
}
# Mechanical sort key: next patch of a stable tag / X.Y.Z version.
# v1.2.0 → 1.2.1. Not a claim that the next official release is 1.2.1.
bump_patch() {
local v=$1 major minor patch
v=${v#v}
IFS=. read -r major minor patch <<<"$v"
printf '%s.%s.%s' "$major" "$minor" "$((10#$patch + 1))"
}
# Strip one leading v if present.
strip_v() {
local v=$1
if [[ "$v" == v* ]]; then
v=${v#v}
fi
printf '%s' "$v"
}
sanitize_branch() {
local b=$1
b=$(printf '%s' "$b" | tr '[:upper:]' '[:lower:]')
b=${b//\//-}
b=${b//_/-}
b=$(printf '%s' "$b" | tr -cd 'a-z0-9-')
b=$(printf '%s' "$b" | tr -s '-')
b=${b#-}
b=${b%-}
if ((${#b} > 32)); then
b=${b:0:32}
b=${b%-}
fi
if [[ -z "$b" ]]; then
b=unknown
fi
printf '%s' "$b"
}
branch_slug() {
local b
b=$(gitc rev-parse --abbrev-ref HEAD)
if [[ "$b" == HEAD ]]; then
b=${BUILD_BRANCH:-${CI_COMMIT_BRANCH:-${GITHUB_REF_NAME:-detached}}}
fi
sanitize_branch "$b"
}
short_sha() {
gitc rev-parse HEAD | cut -c1-7
}
# Build-minute YYYYMMDDTHHMM. SOURCE_DATE_EPOCH (UTC) wins when set, for
# reproducible test packages; otherwise the machine's current local time.
# The minute is not decoration: it is what keeps same-day rebuilds ordered in
# Debian version comparison (0.0.0~20260918T1036… < 0.0.0~20260918T1722…).
build_stamp() {
if [[ -n "${SOURCE_DATE_EPOCH:-}" ]]; then
date -u -d "@${SOURCE_DATE_EPOCH}" +%Y%m%dT%H%M
else
date +%Y%m%dT%H%M
fi
}
# Highest stable tag pointing at HEAD, or empty.
official_tag_at_head() {
local tag best=
while IFS= read -r tag; do
[[ -n "$tag" ]] || continue
is_stable_tag "$tag" || continue
if [[ -z "$best" ]]; then
best=$tag
elif printf '%s\n%s\n' "$best" "$tag" | sort -V | tail -n 1 | grep -qx "$tag"; then
best=$tag
fi
done < <(gitc tag --points-at HEAD)
printf '%s' "$best"
}
# Stable ancestor tag with the fewest commits to HEAD (not sort -V globally).
nearest_stable_tag() {
local tag dist best_dist="" best_tag=""
while IFS= read -r tag; do
[[ -n "$tag" ]] || continue
is_stable_tag "$tag" || continue
dist=$(gitc rev-list --count "${tag}..HEAD")
if [[ -z "$best_dist" ]] || ((dist < best_dist)); then
best_dist=$dist
best_tag=$tag
elif ((dist == best_dist)); then
if printf '%s\n%s\n' "$best_tag" "$tag" | sort -V | tail -n 1 | grep -qx "$tag"; then
best_tag=$tag
fi
fi
done < <(gitc tag --merged HEAD)
printf '%s' "$best_tag"
}
derive_canonical() {
local tag base n sha branch stamp
tag=$(official_tag_at_head)
if [[ -n "$tag" ]]; then
printf '%s' "${tag#v}"
return
fi
sha=$(short_sha)
branch=$(branch_slug)
stamp=$(build_stamp)
base=$(nearest_stable_tag)
if [[ -z "$base" ]]; then
printf '0.0.0~%s.%s+g%s' "$stamp" "$branch" "$sha"
return
fi
n=$(gitc rev-list --count "${base}..HEAD")
printf '%s~%s.%s.%s+g%s' "$(bump_patch "$base")" "$stamp" "$branch" "$n" "$sha"
}
to_docker() {
local v=$1
v=${v//\~/-}
v=${v//+g/.g}
printf '%s' "$v"
}
canonical=
if [[ -n "$override" ]]; then
canonical=$(strip_v "$override")
if [[ "$canonical" == *'_'* ]]; then
echo "Error: version must not contain '_': $canonical" >&2
exit 2
fi
if is_official_version "$canonical"; then
derived=$(derive_canonical)
if [[ "$derived" != "$canonical" ]]; then
echo "Error: --version $canonical looks official but HEAD is $derived" >&2
echo "Official X.Y.Z is allowed only when HEAD exact-matches vX.Y.Z." >&2
exit 2
fi
fi
else
canonical=$(derive_canonical)
fi
if [[ -z "$canonical" ]]; then
echo "Error: empty version" >&2
exit 1
fi
if [[ "$mode" == docker ]]; then
tag=$(to_docker "$canonical")
if [[ "$tag" == *:* || "$tag" == */* || "$tag" == *'~'* ]]; then
echo "Error: docker tag still contains illegal characters: $tag" >&2
exit 1
fi
printf '%s\n' "$tag"
else
printf '%s\n' "$canonical"
fi
+15
View File
@@ -0,0 +1,15 @@
# Builder publish credentials. Copy to the project root as `.env.builder`.
# Do not commit this file. Do not put these keys in the project's `.env`.
# Uncomment and fill the keys for tracks this project uses.
# Shell-exported values override this file.
#
# DEB_SERVER_URL=
# DEB_TOKEN=
# DEB_REPOSITORY=
# DEB_UPLOAD_PATH=
# DOCKER_REGISTRY=
# DOCKER_REPOSITORY=
# IMAGE_TAG=
# PLATFORMS= # optional override; project ARCHES lives in makefile.builder
# COPY_DEST= # local dir, or docker dest container:/absolute/dir
# COPY_AS= # optional extra basenames for the first file
@@ -0,0 +1,57 @@
# Builder 契约文件。放到项目根,用 `make -f makefile.builder <target>`。
# Android 轨道:build 产出 dist/<name>_<version>_<arch>.apk,push 走 copy_artifact.sh。
# ARCHES 是构建机 ISA;APK 可含多 ABI。不要把这些目标写进用户 Makefile。
ARCHES ?= amd64
ARCH ?= $(firstword $(ARCHES))
ifeq ($(strip $(ARCHES)),)
$(error ARCHES must not be empty)
endif
ifneq ($(filter-out amd64 arm64,$(ARCHES)),)
$(error ARCHES entries must be amd64 or arm64)
endif
ifneq ($(filter $(ARCH),$(ARCHES)),$(ARCH))
$(error ARCH must be one of: $(ARCHES))
endif
PROJECT_NAME ?= $(notdir $(CURDIR))
DIST_DIR ?= dist
ifneq ($(BUILDER_SKILL_DIR),)
include $(BUILDER_SKILL_DIR)/scripts/version.mk
else
include $(HOME)/.pouch/skills/builder/scripts/version.mk
endif
BUILDER_SCRIPTS := $(or $(BUILDER_SKILL_DIR),$(HOME)/.pouch/skills/builder)/scripts
APK_OUT := $(DIST_DIR)/$(PROJECT_NAME)_$(VERSION)_$(ARCH).apk
.PHONY: help version arches clean build android push
help:
@echo "$(PROJECT_NAME) $(VERSION)"
@echo " ARCHES=$(ARCHES) ARCH=$(ARCH)"
@echo " make -f makefile.builder build [ARCH=amd64|arm64] - 构建 Release APK"
@echo " make -f makefile.builder version - 打印规范版本"
@echo " make -f makefile.builder arches - 打印架构清单"
@echo " make -f makefile.builder clean - 删除 $(DIST_DIR)"
@echo " make -f makefile.builder push - copy_artifact.sh 拷到 COPY_DEST"
version:
@echo $(VERSION)
arches:
@echo $(ARCHES)
clean:
rm -rf $(DIST_DIR)
# 替换为项目真实 assemble 命令,且必须使用 $(ARCH)。不得 curl/scp/docker cp。
build:
mkdir -p $(DIST_DIR)
cp app-release.apk $(APK_OUT)
android: build
push:
$(BUILDER_SCRIPTS)/copy_artifact.sh $(APK_OUT)
+95
View File
@@ -0,0 +1,95 @@
# Builder 契约文件。放到项目根,用 `make -f makefile.builder <target>`。
# 不要把这些目标写进用户的 Makefile / makefile。
# 初始化时按项目轨道删掉未使用的 deb / docker 段。
# 把 build 的 TODO 换成真实编译命令;禁止在 build / deb / docker 里上传。
# build 必须尊重 $(ARCH)。要发双架构时把 arm64 写进 ARCHES,不要改 .env.builder。
#
# include 路径见 references/contract.md。测试或非标准安装可设 BUILDER_SKILL_DIR。
ARCHES ?= amd64
ARCH ?= $(firstword $(ARCHES))
ifeq ($(strip $(ARCHES)),)
$(error ARCHES must not be empty)
endif
ifneq ($(filter-out amd64 arm64,$(ARCHES)),)
$(error ARCHES entries must be amd64 or arm64)
endif
ifneq ($(filter $(ARCH),$(ARCHES)),$(ARCH))
$(error ARCH must be one of: $(ARCHES))
endif
# Fan-out over ARCHES only when ARCH was not set on the CLI/environment.
_ARCH_FROM_FILE := $(filter file default undefined,$(origin ARCH))
_MULTI_ARCH := $(and $(_ARCH_FROM_FILE),$(filter-out 1,$(words $(ARCHES))))
_PUSH_DEB_ARCHES := $(if $(_ARCH_FROM_FILE),$(ARCHES),$(ARCH))
PROJECT_NAME ?= $(notdir $(CURDIR))
DIST_DIR ?= dist
ifneq ($(BUILDER_SKILL_DIR),)
include $(BUILDER_SKILL_DIR)/scripts/version.mk
else
include $(HOME)/.pouch/skills/builder/scripts/version.mk
endif
BUILDER_SCRIPTS := $(or $(BUILDER_SKILL_DIR),$(HOME)/.pouch/skills/builder)/scripts
.PHONY: help version arches clean build deb docker push push-deb push-docker
help:
@echo "$(PROJECT_NAME) $(VERSION)"
@echo " ARCHES=$(ARCHES) ARCH=$(ARCH)"
@echo " make -f makefile.builder build [ARCH=amd64|arm64] - 构建主产物 / build"
@echo " make -f makefile.builder version - 打印规范版本 / canonical version"
@echo " make -f makefile.builder arches - 打印架构清单 / print ARCHES"
@echo " make -f makefile.builder clean - 删除 $(DIST_DIR)"
@echo " make -f makefile.builder deb - 打 DEB(按 ARCHES;只构建不上传)"
@echo " make -f makefile.builder docker - 本地单平台镜像"
@echo " make -f makefile.builder push / push-deb / push-docker"
version:
@echo $(VERSION)
arches:
@echo $(ARCHES)
clean:
rm -rf $(DIST_DIR)
# 替换为项目真实构建命令,且必须使用 $(ARCH)。不得 curl/scp/docker push。
build:
mkdir -p $(DIST_DIR)
@echo "TODO: build $(PROJECT_NAME) for linux/$(ARCH)"
ifdef _MULTI_ARCH
deb:
$(foreach a,$(ARCHES),$(MAKE) -f makefile.builder --no-print-directory deb ARCH=$(a) &&) true
else
deb: build
mkdir -p $(DIST_DIR)
rm -rf $(DIST_DIR)/packaging-$(ARCH)
cp -a packaging $(DIST_DIR)/packaging-$(ARCH)
sed -e 's/^Architecture:.*/Architecture: $(ARCH)/' \
$(DIST_DIR)/packaging-$(ARCH)/DEBIAN/control \
> $(DIST_DIR)/packaging-$(ARCH)/DEBIAN/control.stamped
mv $(DIST_DIR)/packaging-$(ARCH)/DEBIAN/control.stamped \
$(DIST_DIR)/packaging-$(ARCH)/DEBIAN/control
dpkg-deb --build $(DIST_DIR)/packaging-$(ARCH) $(DIST_DIR)/$(PROJECT_NAME)_$(VERSION)_$(ARCH).deb
endif
docker:
docker build --platform=linux/$(ARCH) -t $(PROJECT_NAME):$(IMAGE_TAG) .
push-deb:
$(BUILDER_SCRIPTS)/upload_deb.sh $(foreach a,$(_PUSH_DEB_ARCHES),$(DIST_DIR)/$(PROJECT_NAME)_$(VERSION)_$(a).deb)
ifdef _ARCH_FROM_FILE
push-docker:
$(BUILDER_SCRIPTS)/publish_docker.sh
else
push-docker:
PLATFORMS=linux/$(ARCH) $(BUILDER_SCRIPTS)/publish_docker.sh
endif
# 单产物项目只保留一条 push。双产物改为:push: push-deb push-docker
push: push-deb
@@ -0,0 +1,57 @@
# Builder 契约文件。放到项目根,用 `make -f makefile.builder <target>`。
# iOS 轨道:build 产出 dist/<name>_<version>_<arch>.ipa,push 走 copy_artifact.sh。
# ARCHES 是构建机 ISA。不要把这些目标写进用户 Makefile。
ARCHES ?= arm64
ARCH ?= $(firstword $(ARCHES))
ifeq ($(strip $(ARCHES)),)
$(error ARCHES must not be empty)
endif
ifneq ($(filter-out amd64 arm64,$(ARCHES)),)
$(error ARCHES entries must be amd64 or arm64)
endif
ifneq ($(filter $(ARCH),$(ARCHES)),$(ARCH))
$(error ARCH must be one of: $(ARCHES))
endif
PROJECT_NAME ?= $(notdir $(CURDIR))
DIST_DIR ?= dist
ifneq ($(BUILDER_SKILL_DIR),)
include $(BUILDER_SKILL_DIR)/scripts/version.mk
else
include $(HOME)/.pouch/skills/builder/scripts/version.mk
endif
BUILDER_SCRIPTS := $(or $(BUILDER_SKILL_DIR),$(HOME)/.pouch/skills/builder)/scripts
IPA_OUT := $(DIST_DIR)/$(PROJECT_NAME)_$(VERSION)_$(ARCH).ipa
.PHONY: help version arches clean build ios push
help:
@echo "$(PROJECT_NAME) $(VERSION)"
@echo " ARCHES=$(ARCHES) ARCH=$(ARCH)"
@echo " make -f makefile.builder build [ARCH=amd64|arm64] - 构建 IPA"
@echo " make -f makefile.builder version - 打印规范版本"
@echo " make -f makefile.builder arches - 打印架构清单"
@echo " make -f makefile.builder clean - 删除 $(DIST_DIR)"
@echo " make -f makefile.builder push - copy_artifact.sh 拷到 COPY_DEST"
version:
@echo $(VERSION)
arches:
@echo $(ARCHES)
clean:
rm -rf $(DIST_DIR)
# 替换为项目真实 xcodebuild/export 命令,且必须使用 $(ARCH)。不得 curl/scp/docker cp。
build:
mkdir -p $(DIST_DIR)
cp app.ipa $(IPA_OUT)
ios: build
push:
$(BUILDER_SCRIPTS)/copy_artifact.sh $(IPA_OUT)
@@ -0,0 +1,55 @@
# declarative-openspec-loop
你提供「怎样算符合预期」的校验方式,Agent 按 OpenSpec 走 propose → apply → 校验,不通过就分析原因再迭代,直到通过。
## 什么时候使用
- 已经能说清目标,也有办法判断结果对不对,希望 Agent 自动做到通过为止。
- 会说「声明式编程」「按预期循环」「做到校验通过」。
- 不适合:还在选型、还没有校验手段、只想改一小处且自己盯着看。
## 安装
```bash
pouch add declarative-openspec-loop # 当前项目
pouch add declarative-openspec-loop -g # 全局
```
## 使用前准备
- 本机已安装 [openspec](https://openspec.dev/) CLI。
- 当前 Agent 已安装 OpenSpec 的三个子 skill:`openspec-explore`、`openspec-propose`、`openspec-apply-change`。
- 你必须给出校验方式,缺了 Agent 会先问,不会空跑。可用下面任意组合:
- 校验命令,退出码 0 即通过,比如 `make test-one2`
- 预期文档,里面有校验项表
- 清单:日志关键字、输出文件、数值容差
- 自由描述:Agent 会先归纳成可执行检查,向你确认后再跑
## 使用示例
```text
按声明式循环做:让登录失败时返回统一错误码。
校验:跑 pytest tests/test_login.py,全部 green 即通过。最多 3 轮。
```
```text
目标:拟合轮廓与 docs/tmp/expect_desc.md 一致。
校验:make test-one2,且日志覆盖文档「校验项」表全部 8 项。
```
## Agent 会做什么
1. 记下 change 名称和校验方式,写到 `docs/tmp/<change>/expectation.md`。
2. 必要时先探索;然后 propose 出 proposal / design / tasks,每个任务尽量带验收。
3. apply 实现,再跑你给的校验。
4. 失败按原因分级:小 bug 直接修;任务遗漏就补 tasks;方向错了才改设计。默认最多 5 轮(你可改上限)。同一错误连续 2 轮会停下来问你。
5. 通过后给出变更摘要和思路,过程记录留在 `docs/tmp/<change>/`。
不会在没校验方式时开跑,也不会到上限后自己放弃或无限重试。
## 如何判断完成
- 你指定的校验命令退出码 0,或校验项全部满足。
- Agent 给出变更文件列表和思路。
- `docs/tmp/<change>/` 里有 `expectation.md`、每轮 `iteration_*.md` 和校验原始输出。
- 到上限或反复失败时停下,并说明当前进度和未通过项,而不是假装完成。
+32 -16
View File
@@ -5,31 +5,45 @@
支持三种用法:
- **独立配置中心**:一个专门的 Git 仓库管所有机器的所有 Compose 服务(如 app00)
- **项目内 Compose 环境**:在普通项目里放 `.skiff/deployer/{prod,test,dev}/`,
- **项目内 Compose 环境**:在普通项目里放 `.pouch/deployer/{prod,test,dev}/`,
把这个项目的生产/测试/开发环境用 rsync + docker compose 部署
- **Argo CD / GitOps**:项目里放 `.skiff/deployer/argocd.yaml`;Agent 改 GitOps 并开 MR,
- **Argo CD / GitOps**:项目里放 `.pouch/deployer/argocd.yaml`;Agent 改 GitOps 并开 MR,
你合并后由 Argo CD 同步。仓库既可以只写 Git 地址(部署时浅 clone),也可以指定本机已有目录。
## 什么时候使用
- 给新项目初始化 deployer:检查缺什么,引导补 `.pouch/deployer/`
- 想用一套固定流程把本地改好的 Docker Compose 配置发到某台服务器
- 要升级某个服务的镜像版本、重启服务、看远程容器状态和日志
- 有编译好的 .deb 包要装到某台机器上(scp 上传安装,或从 URL 直接拉)
- 新加一个服务、把服务从一台机器挪到另一台、或下线旧服务
- 想给当前项目加 prod/test/dev 三套远程环境并随时部署其中一套
- ACK 说「重新布测试环境」或跑回归前要先拉起 `.skiff/deployer/test`
- ACK 说「重新布测试环境」或跑回归前要先拉起 `.pouch/deployer/test`
- 需要一张「哪台机器跑哪些服务」的清单
- 镜像要进 Kubernetes,走 Argo CD:改 GitOps、开 MR、合并后部署
## 安装
```bash
pouch add deployer # 当前项目
pouch add deployer -g # 全局
```
不要 `pouch init deployer`。对新项目说「用 deployer 初始化」。
## 使用前准备
- Compose:本机 Python 3、`rsync`、`ssh`;目标机器 Docker Compose v2;`~/.ssh/config` 免密(或 agent)
- Compose:本机 Python 3、`ssh`;有 `rsync` 更好,没有则 sync 用 `scp` 兜底;目标机器 Docker Compose v2;`~/.ssh/config` 免密(或 agent)
- 知道每个 Compose 服务的运行时数据放在哪(这些目录不能被同步覆盖)
- Argo CD:能 clone/push GitOps 仓库(SSH 或已登录的 HTTPS),并能开 MR(GitLab `glab` / GitHub `gh` / Gitea `tea`)。本机不必长期放一份 GitOps checkout。
## 使用示例
```text
# 新项目
用 deployer 初始化这个项目
给这个项目接上 test 环境,SSH 别名是 my-vps
# 独立配置中心
帮我把 vyyo1/naiveproxy 的配置改完部署上去
升级 vora3/gpt-load 的镜像版本
@@ -43,8 +57,9 @@ vhom1 上那个 naiveproxy 为什么 sync 失败?
web1 能出网,直接让它从 https://... 把包拉下来装
# 项目内环境
给这个项目建好 .skiff/deployer,prod 和 test 分别放到两台机器上
给这个项目建好 .pouch/deployer,prod 和 test 分别放到两台机器上
把 test 环境重新部署一下
本机测试镜像已经有了,直接重启,别 sync
prod 的 compose 加个 redis,改完发上去
# Argo CD
@@ -56,39 +71,40 @@ GitOps 我已经 clone 在 ../infra-gitops,用那个目录开 MR
## Agent 会做什么
1. 读服务/环境目录(及共享的父目录)的 `_config.yaml`,确定目标机器和远程路径;
项目内布局从 `.skiff/deployer/` 自动发现,无需额外配置
2. 用 skill 自带脚本把本地目录同步到远程(rsync,自动排除 `data/`、`_data/`)
3. 在远程执行对应的 `docker compose` 操作(启动 / 重建 / 升级 / 重启)
项目内布局从 `.pouch/deployer/` 自动发现,无需额外配置
2. 目标是本机且镜像已在本机 docker:不 sync、不 SSH,在该环境目录直接 `docker compose`
3. 否则用 skill 自带脚本把本地目录同步到远程(优先 rsync,没有则 scp;自动排除 `data/`、`_data/`),再远程 `docker compose`(启动 / 重建 / 升级 / 重启)
4. deb 包安装走独立脚本:scp 上传到暂存目录后远程 apt 安装,失败自动修依赖
5. 同步后查看容器状态和日志确认生效
6. 只针对你指定的那一个服务操作,不会批量动整台机器
7. Argo CD:读 `.skiff/deployer/argocd.yaml`,浅 clone 或使用 `repo_dir`,改清单,推分支开 MR,停下来等你合并;
7. Argo CD:读 `.pouch/deployer/argocd.yaml`,浅 clone 或使用 `repo_dir`,改清单,推分支开 MR,停下来等你合并;
不直接 kubectl 发布,不把 Harbor/TLS 密钥提交进 Git
项目内布局下,远程目录名自动带上项目前缀(如 `my-project-prod`),
避免同一台机器上多个项目的同名环境互相覆盖;需要固定名字时在 `_config.yaml` 写 `name:`。
重要边界:同步使用 `--delete`,远程多余的文件会被删除;数据库、证书等运行时数据
必须放在排除目录或远程绝对路径挂载。涉及删除数据卷、清理远程文件的操作会先向你确认。
重要边界:rsync 同步使用 `--delete`,远程多余的文件会被删除;scp 兜底不会删。
数据库、证书等运行时数据必须放在排除目录或远程绝对路径挂载。涉及删除数据卷、
清理远程文件的操作会先向你确认。
## 如何判断完成
- 脚本输出显示同步完成、远程命令执行成功
- `ps` 显示容器 Up、`logs` 无报错;升级后镜像 tag 与配置一致
- 本机:`docker compose ps` 容器 Up。远程:sync 成功且 `remote.py ps` 容器 Up
- `logs` 无报错;升级后镜像 tag 与配置一致
- deb 安装后 `ssh <node> dpkg -l` 能看到目标包,服务能正常启动
- 域名/端口类服务能 curl 通
- Argo CD:给出 MR 链接;合入后 Application Synced,域名/healthz 可访问
## Argo CD 的两种接法
在业务项目里放 `.skiff/deployer/argocd.yaml`。Agent 只改 GitOps 并开 MR,**你合并之后** Argo CD 才部署。镜像、namespace、域名、Secret 名以 GitOps 清单为准,不必在这个文件里再抄一遍。
在业务项目里放 `.pouch/deployer/argocd.yaml`。Agent 只改 GitOps 并开 MR,**你合并之后** Argo CD 才部署。镜像、namespace、域名、Secret 名以 GitOps 清单为准,不必在这个文件里再抄一遍。
### 1. 只写仓库地址(默认)
本机不用长期放 GitOps 仓库。部署时 Agent 浅 clone 到临时目录,改完开 MR,用完删掉。
```yaml
# .skiff/deployer/argocd.yaml
# .pouch/deployer/argocd.yaml
repo: git@git.example.com:org/infra-gitops.git
```
@@ -106,7 +122,7 @@ repo: git@git.example.com:org/infra-gitops.git
GitOps 仓库已经 checkout 在旁边时,写 `repo_dir`,Agent 直接进这个目录改、推分支、开 MR。
```yaml
# .skiff/deployer/argocd.yaml
# .pouch/deployer/argocd.yaml
repo: git@git.example.com:org/infra-gitops.git
repo_dir: ../infra-gitops
```
+55 -243
View File
@@ -1,276 +1,88 @@
---
name: deployer
description: >-
管理两类部署:多机 Docker Compose(仓库存 compose.yaml 与静态配置,本 skill 脚本
同步到 SSH 节点后 docker compose 应用),以及 Argo CD GitOps(改 GitOps 仓库清单、
开 PR/MR,用户合并后由 Argo CD 同步)。当用户要求部署、同步、升级、重启远程
Compose 服务,向节点装 deb,新增/迁移/下线服务,梳理节点清单,make deploy TGT、
_config.yaml、rsync、tar over SSH、NAS 部署失败;或要求 ArgoCD / GitOps / K8s
部署、更新 Application、升镜像 tag、开 MR 让用户合并部署;或 ACK 要求拉起/
重布项目测试环境时使用。
初始化或检查项目部署配置;管理多机 Docker Compose(脚本同步后远程
compose)与 Argo CD GitOps(改清单开 MR,用户合并后同步)。触发词:初始化
deployer、部署、sync、升级、装 deb、ArgoCD、GitOps;ACK 拉起测试环境时也可使用。
---
# deployer:Compose 节点与 Argo CD GitOps
两条轨道,配置都在仓库里,方法由本 skill 提供。
- **Compose**:本地改 `compose.yaml` → 脚本同步到 SSH 节点 → 远程 `docker compose`。
- **Argo CD**:改 GitOps 仓库清单 → 开 PR/MR → 用户合并 → Argo CD 同步。不要用
Compose 的 `sync.py`/`remote.py` 去推集群。
- **Argo CD**:改 GitOps 仓库清单 → 开 PR/MR → 用户合并 → Argo CD 同步。不要用 Compose 的 `sync.py`/`remote.py` 去推集群。
---
开始时解析当前 `SKILL.md` 所在目录,记为 `<skill-dir>`。优先
`git rev-parse --show-toplevel` 解析项目根。不要创建 `.pouch/deployer/` 之外的
假配置中心,也不要用 `pouch init deployer`。
## 何时使用
## 选择模式
- 部署 / 同步 / 升级 / 重启某个远程 Docker Compose 服务
- 向节点安装 deb 包:scp 上传本地 .deb 后 dpkg/apt 安装,或从 URL 远程拉取安装
- 新增、迁移、下线一个服务;梳理「哪台机器跑什么」
- sync 失败排查、证书丢失、改了配置不生效等运维问题
- 提到 `make deploy TGT=...`、`TGT=`、`_config.yaml`、rsync/tar 同步
- Argo CD / GitOps / 集群部署:新增 Application、改清单、升镜像 tag、开 MR 等用户合并
- ACK Coordinator 拉起或重布项目测试环境(`.skiff/deployer/<env>`)
- 初始化、接入 deployer,或给新项目建测试/生产环境:执行「初始化」。
- 检查 `.pouch/deployer`、node、compose 是否齐全:执行「检查」。
- 部署、重新部署、同步、升级、重启、布测试环境、装 deb、新增/下线服务:执行「Compose 操作」。发现不了服务或解析不出 node 时停止,转入「初始化」。不要静默初始化。
- ArgoCD / GitOps / 开 MR 部署:执行「Argo CD」。步骤见 [argocd.md](references/argocd.md)。
- 两者都有且意图不清:先问。
- 不适用:没有 `.pouch/deployer/` 的随手 `docker compose`、Nomad、常规 `kubectl apply`、构建并推送镜像(走 builder)。本机 node 仍走本 skill,镜像已在本机则跳过 sync。
## 不适用
## 边界
- 单机 docker 日常使用(无多机同步诉求)
- Nomad,或绕过 GitOps 用 `kubectl apply` 当常规发布
- 构建并推送镜像(走 builder);本 skill 只改 GitOps 里对该镜像的引用
- 所有 sync/up/recreate/upgrade/restart 必须按单服务执行,禁止节点级批量。
- `rsync` 带 `--delete`:运行时数据必须在 `data/`、`_data/` 或远程绝对路径挂载,否则会被清掉。
- 仓库只放静态配置;证书、数据库、上传文件不进 Git,也不进同步范围。`unused/` 不参与发现与部署。
- `node` 是 `~/.ssh/config` 的 Host 别名(可用 `user@host`)。用户没给别名就不要写假 node。
- 密钥不入库。Compose 优先放远程 `.env`;Argo CD 的 dockerconfigjson / TLS 私钥只存在集群 Secret。
- 不 `--force` 推送、不硬 reset,除非用户明确要求。Argo CD 不直接推默认分支。
- 镜像固定 tag,不用 `:latest`;成对升级的服务要同步升。
- ACK 调用不能把范围扩到生产环境、Argo 合入或节点级批量。
---
## 初始化
## 核心模型(先读懂再动手)
独立配置中心仓库(已设 `DEPLOYER_ROOT`)只做检查,不要改成项目内布局。`_config.yaml` 字段见 [config-reference.md](references/config-reference.md)。
### 轨道选择
1. 探测:`.pouch/deployer/`、根目录 compose、`Dockerfile`、ACK `intents.testEnvironment`、`argocd.yaml`。
2. Compose 与 Argo 都有且意图不清:先问。两边都要也可以,必须分开确认。
3. Compose / 新项目(ACK 默认需要 `test`):默认只建 `test`;`prod`/`dev` 用户点名再加。问 SSH Host 别名;没给不要写假 node,目录可建、列为待配置。有根目录 compose:提议迁到 `.pouch/deployer/<env>/`,确认后才动。只有 Dockerfile:可给单服务 compose 草稿,用户确认后写入,不发明多服务网格。
4. Argo:只问 GitOps `repo`(或 `repo_dir`),写 `.pouch/deployer/argocd.yaml`。不 clone、不开 MR、不 `kubectl apply`。
5. 运行(不 SSH、不 up):
| 信号 | 轨道 |
|------|------|
| ArgoCD / GitOps / 集群 / 开 MR 部署 / 项目有 `.skiff/deployer/argocd.yaml` | Argo CD,见 [argocd.md](references/argocd.md) |
| sync、`TGT=`、某台机器、`compose.yaml` | Compose(下文布局与步骤) |
| 两者都有且意图不清 | 先问 |
### Compose
- **仓库只放数据**:`compose.yaml`、Caddyfile、Traefik 动态配置等静态配置进 Git;
运行时数据(证书、数据库、上传文件)永不进 Git,也永不参与同步范围。
- **每个可部署服务目录必须有 `compose.yaml`**,且能解析出目标节点 `node`
(来自该目录、部署根或祖先目录的 `_config.yaml`,或父目录名恰为 SSH Host 别名)。
- **`node` 即 SSH Host 别名**(`~/.ssh/config`),支持 `user@host` 形式。
- `unused/` 下不参与自动发现与部署。
### Argo CD
源项目 `.skiff/deployer/argocd.yaml`:`repo` 写 Git 地址(部署时浅 clone),或加 `repo_dir` 用已有 checkout。
改 GitOps 清单,不要改 Compose 脚本。密钥不入库。两种接法见 skill README,步骤见 [argocd.md](references/argocd.md)。
### 两种 Compose 布局
**A. 独立配置中心仓库**(如 app00):仓库根即部署根,
`DEPLOYER_ROOT=/path/to/repo` 指定后按仓库内相对路径操作:
```
repo/
├── _config.yaml # 可选,全局默认
├── vyyo1/_config.yaml # node: vyyo1(主机目录)
│ └── naiveproxy/ # 服务目录:compose.yaml + 可选 _config.yaml
└── unused/
```bash
python3 -I -S <skill-dir>/scripts/deploy/check.py --project <project-root>
```
远程目录名 = 目录末级名:`vyyo1/naiveproxy` → `/opt/app/naiveproxy`。
6. 缺 `node` / compose / ssh 别名 = 部分完成或阻塞。不覆盖已有 compose/`_config.yaml`。除非用户明确要求,不部署、不提交。
**B. 项目内环境布局**:项目根放 `.skiff/deployer/{prod,test,dev}/`,
每个环境一个目录。从项目内任意位置运行脚本即自动发现(也可用 `DEPLOYER_ROOT`
显式指定),无需环境变量:
```text
## deployer 初始化:完成 | 部分完成 | 阻塞
```
my-project/
├── src/ ... # 项目本体
└── .skiff/deployer/
├── _config.yaml # 三个环境共享默认(node/base_path 等)
├── argocd.yaml # 可选,Argo CD 指针(不是 compose 环境)
├── prod/
│ ├── compose.yaml # 生产 compose 与配置
│ └── _config.yaml # 环境级覆盖
├── test/compose.yaml
└── dev/compose.yaml
已具备: …
待配置: 路径 + 字段 + 可粘贴示例 + 缺了会挡住哪步
工具链: ssh / rsync(无 rsync 时 sync 用 scp 兜底,不擅自安装)
下一步: 一句话
```
项目模式下远程目录名自动加项目前缀 `{git仓库名}-{env}`
(如 `my-project-prod`),防止同主机多项目的同名环境互相覆盖;
`_config.yaml` 写 `name:` 可显式指定。
## 检查
只读。运行 `scripts/deploy/check.py --project <project-root>`,用同一报告格式,标题改为 `## deployer 检查:…`。不写文件、不 SSH。用户明确要求修复后再转入初始化。
## 被 ACK 调用
ACK 的「运行测试环境」和回归前布环境会加载本 skill,对项目
`.skiff/deployer/<env>`(通常是 `test`)按下面 Compose 轨道执行。ACK 只负责何时
布、把访问地址写入 `deliveryRuns`;不要把本 skill 的脚本复制进 ACK。生产环境、
Argo CD 合入和节点级批量操作仍须用户明确要求,不能因为 ACK 调用就扩大范围。
ACK 的「运行测试环境」和回归前布环境会加载本 skill,按「Compose 操作」的默认路径布测试环境。ACK 只负责何时布、把访问地址写入 `deliveryRuns`;不要把本 skill 的脚本复制进 ACK。生产环境、Argo CD 合入和节点级批量仍须用户明确要求。
## 步骤
## Compose 操作
### Compose 轨道
重启 / 重新部署 / 同步 / 布测试环境走默认路径。不要读 compose.md、不要初始化、不要读 Makefile。
#### 0. 定位部署根
1. 在项目根执行:`python3 -I -S <skill-dir>/scripts/deploy/list.py`
2. 只操作**一个**服务:用户点名的 env;「测试」→ `test`,没有 `test` 就用非 `prod` 的那个。未点名 `prod` 不要动它。
3. 不要改 compose tag、不要构建镜像、不要看其它环境。
4. 本机镜像:node 的 ssh `HostName` 是 `127.0.0.1`/`localhost`,且 `docker image inspect` 看得到 compose 里的 tag → **不要** `sync.py` / `remote.py`。进 `.pouch/deployer/<env>/` 执行 `docker compose up -d --force-recreate`,再 `ps`。完。
5. 否则才 sync + remote:`python3 -I -S <skill-dir>/scripts/deploy/sync.py <svc>` 然后 `python3 -I -S <skill-dir>/scripts/deploy/remote.py <svc> recreate`。只要重启:`remote.py <svc> restart`。
6. 远程路径验证:`remote.py <svc> ps`;异常再 `logs`。完。
skill 目录下的 `scripts/deploy/` 是通用部署工具链(lib/sync/remote/list),
不依赖具体项目路径。部署根按以下顺序解析:
compose 用 `image:` 且用户说刚合并了代码:现有 tag 对不上这次提交时,**一句问**要不要先重建镜像,然后停。不要自己改 tag、不要走 builder。
1. 环境变量 `DEPLOYER_ROOT` 显式指定(独立配置中心仓库用这个)
2. 从当前目录向上找 `.skiff/deployer/`(项目内环境布局自动发现)
3. skill 安装位置兜底(仅用于查看,没有可部署服务)
新增/下线、deb、镜像 tag 升级、NAS 失败:再读 [compose.md](references/compose.md)。字段见 [config-reference.md](references/config-reference.md)。
```bash
# <skill-dir> = 本 SKILL.md 所在目录,先解析出来记下
# 布局 A:显式指定仓库根
export DEPLOYER_ROOT=/path/to/your-compose-repo
python3 <skill-dir>/scripts/deploy/list.py
## Argo CD
# 布局 B:在项目内直接跑即可(cwd 在项目里)
python3 <skill-dir>/scripts/deploy/list.py
```
#### 1. 摸底:列出服务与节点
上一步的 `list.py` 输出全部服务与节点分布;新增环境/服务后重跑确认被发现。
项目布局下 `prod/test/dev` 各显示为 `{项目名}-{env}`。`argocd.yaml` 不会被 list.py 当成 compose 服务。
#### 2. 解析单个服务
```bash
# 查看 node、远程路径、排除规则(sync.py 干跑会打印这些信息)
python3 <skill-dir>/scripts/deploy/sync.py <service-path>
```
或直接读服务目录及祖先的 `_config.yaml`。
#### 3. 命令选择(语义严格区分)
| 意图 | 命令 |
|------|------|
| 只同步文件,不动容器 | `sync.py <svc>` |
| 应用 compose/配置变更 | `sync.py <svc> && remote.py <svc> up` |
| 改配置后强制重建 | `remote.py <svc> recreate`(配合前置 sync) |
| 镜像 tag 变更升级 | `sync.py <svc> && remote.py <svc> upgrade` |
| 仅重启,不同步文件 | `remote.py <svc> restart` |
| 排查 | `remote.py <svc> ps` / `remote.py <svc> logs` |
#### 4. 项目侧 Makefile(可选薄封装)
若项目有 Makefile 封装,命令形如 `make deploy TGT=<服务路径>`。
没有 Makefile 时直接调 python 脚本即可,不要新建封装层。
#### 5. 新增服务 / 环境 checklist
独立仓库布局:
1. 在合适分类目录创建服务文件夹,写 `compose.yaml`
2. 在服务目录或祖先目录放 `_config.yaml`(至少能解析出 `node`)
3. 有运行时目录 → 加进 `sync_exclude`
4. 远程首次建目录:`ssh <node> "mkdir -p <base_path>/<name>"`
5. 首次部署:sync + up
6. 验证:ps + logs,必要时 curl/ssh 检查端口
项目环境布局:
1. 项目根建 `.skiff/deployer/{env}/`(env 通常为 prod/test/dev)
2. 每个环境写 `compose.yaml`;三个环境共享的 node/base_path 放
`.skiff/deployer/_config.yaml`
3. 环境有差异(不同主机、不同排除项)→ 在该环境的 `_config.yaml` 覆盖
4. 同名冲突或需要固定远程目录名 → `_config.yaml` 写 `name:`
5. 首次部署前确认目标主机的远程目录不存在旧内容(rsync `--delete` 会清掉)
#### 6. 下线服务
独立仓库布局:配置移入 `unused/`(自动脱离发现体系),远程按需手动清理:
`ssh <node> "cd <base_path>/<name> && docker compose down"`,数据卷按需保留或删除。
项目环境布局:删除对应 `.skiff/deployer/{env}/` 目录即可脱离发现体系,远程清理同上。
#### 7. 向节点安装 deb 包
`deb.py` 把 deb 包发到节点并安装。目标两种写法:仓库内目录
(复用 `_config.yaml` 继承链解析 node/port/identity_file,如 `hosts/web1`),
或裸 SSH 别名 / `user@host`(须在 `~/.ssh/config` 中,可加 `--port`/`--identity`)。
```bash
# 本地 .deb → scp 上传 → 远程 apt 安装(失败自动 apt -f 修依赖),成功后删暂存包
python3 <skill-dir>/scripts/deploy/deb.py <target> push ./foo_1.0_amd64.deb --yes
# 仅上传到远程暂存目录(默认 {base_path}/.debs;裸主机为 /tmp/deployer-debs)
python3 <skill-dir>/scripts/deploy/deb.py <target> scp ./foo_1.0_amd64.deb
# 安装该节点暂存目录里已上传的全部 .deb(配合 scp 分步操作)
python3 <skill-dir>/scripts/deploy/deb.py <target> dpkg --yes
# 远程直接从 URL 下载安装(机器能出网时免上传)
python3 <skill-dir>/scripts/deploy/deb.py <target> apt https://example.com/foo_1.0_amd64.deb --yes
```
- 非 root 用户走 `sudo -n`(需配好免密 sudo);`--yes` 传 `-y` 免交互,
无终端交互能力,没配 sudo 免密/密钥时会直接失败。
- 升级同版本号前想先看包信息:`ssh <node> "dpkg -I <暂存路径>"`;
装完验证:`ssh <node> "dpkg -l | grep <pkg>"`。
### Argo CD 轨道
完整步骤与 `argocd.yaml` 字段见 [argocd.md](references/argocd.md)。
1. 读项目 `.skiff/deployer/argocd.yaml`(无则只问 Git 地址,写成 `repo:`)。
有 `repo_dir` 则用该目录;否则把 `repo` 浅 clone 到临时目录,用完删除。
2. 在工作副本里按**已有应用惯例**新增 Application,或只改镜像 tag / 清单。
3. 从最新默认分支拉出分支,commit、push,用 `glab`/`gh`/`tea` 开 MR;CLI 对项目 404 则把
`git push` 给出的网页建单链接交给用户。
4. **停在 MR**,不合并、不 `kubectl apply` 工作负载。
5. 无 app-of-apps 时提醒用户首次 `kubectl apply` 那份 `application.yaml`。
6. Harbor 拉镜像 Secret、TLS Secret 不入库;只在工作负载所在 ns 准备,可从其他 ns 拷贝。
---
## 注意事项
- **禁止节点级批量操作**:所有 sync/up/recreate/upgrade/restart 必须按单服务执行。
批量升级风险过高,逐个来。
- **rsync 带 `--delete`**:远程多余文件会被删除。运行时数据必须放在
默认排除的 `data/`、`_data/`,或 compose 挂载的远程绝对路径
(如 `/data01/docker/<svc>/`),否则会被清掉。
- **镜像固定 tag**,不用 `:latest` 漂移;成对升级的服务(如 proxy 客户端/服务端)要同步升。
- **密钥**:Compose 优先放远程 `.env`;Argo CD 的 dockerconfigjson / TLS 私钥只存在集群 Secret。
不要提交新密钥进 Git。
- **Git 安全**:不 `--force` 推送、不硬 reset,除非用户明确要求。Argo CD 轨道不直接推默认分支。
- **NAS / Synology 特例**:部分 NAS 的 SSH 用户禁用 rsync 协议(Permission denied)。
表现是 sync 报错但 ssh 正常。处理顺序:
1. 该节点 `_config.yaml` 写真实 `base_path`(如 `/volume1/docker`,避开符号链接路径)
2. 仍失败则手动 tar over SSH 推送:
```bash
tar czf - -C <服务目录> . --exclude='data' --exclude='_data' \
| ssh <node> "mkdir -p <base_path>/<name> && cd <base_path>/<name> && tar xzf -"
ssh <node> "cd <base_path>/<name> && /usr/local/bin/docker compose up -d"
```
tar 不会删除远程多余文件;需清理旧文件时手动 SSH 删除。
3. Synology 上 docker 路径可能是 `/usr/local/bin/docker`
## 验证
- Compose:`list.py` 输出全部服务与节点分布,数量与预期一致
- 每次 sync/deploy 后 `remote.py <svc> ps` 容器 Up、`logs` 无报错
- 升级后额外确认镜像 tag 与 compose.yaml 一致
- 改 Traefik/Caddy 路由后 curl 对应域名验证生效
- Argo CD:MR 可打开且含本次清单;用户合并后 Application Synced;有 Ingress 则 curl healthz
## scripts/
| 文件 | 用途 |
|------|------|
| `scripts/deploy/lib.py` | 解析服务目录、合并继承 `_config.yaml`、SSH/rsync/scp 参数构造 |
| `scripts/deploy/sync.py` | rsync -avz --delete 同步;无 rsync 时 tar over SSH 兜底 |
| `scripts/deploy/remote.py` | SSH 远程 docker compose:up/recreate/restart/upgrade/ps/logs |
| `scripts/deploy/deb.py` | deb 包分发安装:push(scp+apt)/scp/dpkg/apt(URL) |
| `scripts/deploy/list.py` | 扫描全部可部署服务 |
## references/
| 文件 | 用途 |
|------|------|
| `references/config-reference.md` | `_config.yaml` 字段完整说明与继承合并规则 |
| `references/argocd.md` | Argo CD 轨道:`argocd.yaml`、新增/升级、开 MR、ns 级 Secret |
读 [argocd.md](references/argocd.md)。读 `.pouch/deployer/argocd.yaml`(无则只问 Git 地址写成 `repo:`)。按已有应用惯例改清单,从默认分支拉出分支开 MR。**停在 MR**,不合并、不 `kubectl apply` 工作负载。无 app-of-apps 时提醒用户首次 apply 那份 `application.yaml`。Harbor / TLS Secret 不入库。
+163
View File
@@ -0,0 +1,163 @@
# Compose 轨道
同步文件、远程 `docker compose`、装 deb、新增/下线服务。Argo CD 不走本文件。
## 目录
- [两种布局](#两种布局)
- [定位部署根](#定位部署根)
- [列出服务](#列出服务)
- [命令选择](#命令选择)
- [新增服务 / 环境](#新增服务--环境)
- [下线服务](#下线服务)
- [安装 deb](#安装-deb)
- [NAS / rsync 失败](#nas--rsync-失败)
- [验证](#验证)
## 两种布局
**A. 独立配置中心仓库**:仓库根即部署根,`DEPLOYER_ROOT=/path/to/repo` 指定后按仓库内相对路径操作:
```
repo/
├── _config.yaml # 可选,全局默认
├── vyyo1/_config.yaml # node: vyyo1(主机目录)
│ └── naiveproxy/ # 服务目录:compose.yaml + 可选 _config.yaml
└── unused/
```
远程目录名 = 目录末级名:`vyyo1/naiveproxy` → `/opt/app/naiveproxy`。
**B. 项目内环境布局**:项目根放 `.pouch/deployer/{prod,test,dev}/`,每个环境一个目录。从项目内任意位置运行脚本即自动发现(也可用 `DEPLOYER_ROOT` 显式指定):
```
my-project/
├── src/ ...
└── .pouch/deployer/
├── _config.yaml # 三个环境共享默认(node/base_path 等)
├── argocd.yaml # 可选,Argo CD 指针(不是 compose 环境)
├── prod/
│ ├── compose.yaml
│ └── _config.yaml # 环境级覆盖
├── test/compose.yaml
└── dev/compose.yaml
```
项目模式下远程目录名自动加项目前缀 `{git仓库名}-{env}`(如 `my-project-prod`);`_config.yaml` 写 `name:` 可显式指定。
## 定位部署根
`scripts/deploy/` 是通用工具链,不依赖具体项目路径。部署根顺序:
1. 环境变量 `DEPLOYER_ROOT`(独立配置中心仓库用这个)
2. 从当前目录向上找 `.pouch/deployer/`(项目内环境布局)
3. skill 安装位置兜底(仅查看,没有可部署服务)
```bash
# <skill-dir> = 本 skill 的 SKILL.md 所在目录
# 布局 A
export DEPLOYER_ROOT=/path/to/your-compose-repo
python3 <skill-dir>/scripts/deploy/list.py
# 布局 B:cwd 在项目里即可
python3 <skill-dir>/scripts/deploy/list.py
```
## 列出服务
`list.py` 输出全部服务与节点分布;新增环境/服务后重跑确认被发现。项目布局下 `prod/test/dev` 各显示为 `{项目名}-{env}`。`argocd.yaml` 不会被当成 compose 服务。
查看单个服务的 node、远程路径、排除规则:
```bash
python3 <skill-dir>/scripts/deploy/sync.py <service-path>
```
或读服务目录及祖先的 `_config.yaml`。
## 命令选择
语义严格区分,必须按**单服务**执行:
| 意图 | 命令 |
|------|------|
| 本机已有镜像(node 是 localhost,image 已在本机 docker) | 不要 sync。在 env 目录 `docker compose up -d --force-recreate` |
| 重新部署远程环境 | `sync.py <svc> && remote.py <svc> recreate` |
| 只同步文件,不动容器 | `sync.py <svc>` |
| 应用 compose/配置变更 | `sync.py <svc> && remote.py <svc> up` |
| 改配置后强制重建 | `remote.py <svc> recreate`(配合前置 sync) |
| 镜像 tag 变更升级 | `sync.py <svc> && remote.py <svc> upgrade` |
| 仅重启,不同步文件 | `remote.py <svc> restart` |
| 排查 | `remote.py <svc> ps` / `remote.py <svc> logs` |
脚本一律 `python3 -I -S <skill-dir>/scripts/deploy/<name>.py`。有 `make deploy TGT=` 才用 Makefile;没有就直接调脚本,不要读 Makefile 去发明封装。
`sync.py` 使用 `rsync -avz --delete`;本机无 rsync 时 scp 兜底(不 `--delete`)。运行时数据必须放在默认排除的 `data/`、`_data/`,或 compose 挂载的远程绝对路径,否则会被清掉。
## 新增服务 / 环境
独立仓库布局:
1. 在合适分类目录创建服务文件夹,写 `compose.yaml`
2. 在服务目录或祖先目录放 `_config.yaml`(至少能解析出 `node`)
3. 有运行时目录 → 加进 `sync_exclude`
4. 远程首次建目录:`ssh <node> "mkdir -p <base_path>/<name>"`
5. 首次部署:sync + up
6. 验证:ps + logs,必要时 curl/ssh 检查端口
项目环境布局:
1. 项目根建 `.pouch/deployer/{env}/`(env 通常为 prod/test/dev)
2. 每个环境写 `compose.yaml`;共享的 node/base_path 放 `.pouch/deployer/_config.yaml`
3. 环境有差异 → 在该环境的 `_config.yaml` 覆盖
4. 同名冲突或需要固定远程目录名 → `_config.yaml` 写 `name:`
5. 首次部署前确认目标主机的远程目录不存在旧内容(rsync `--delete` 会清掉)
## 下线服务
独立仓库布局:配置移入 `unused/`(自动脱离发现体系),远程按需手动清理:
`ssh <node> "cd <base_path>/<name> && docker compose down"`,数据卷按需保留或删除。
项目环境布局:删除对应 `.pouch/deployer/{env}/` 目录即可脱离发现体系,远程清理同上。
## 安装 deb
`deb.py` 把 deb 包发到节点并安装。目标两种写法:仓库内目录(复用 `_config.yaml` 继承链解析 node/port/identity_file,如 `hosts/web1`),或裸 SSH 别名 / `user@host`(须在 `~/.ssh/config` 中,可加 `--port`/`--identity`)。
```bash
# 本地 .deb → scp 上传 → 远程 apt 安装(失败自动 apt -f 修依赖),成功后删暂存包
python3 <skill-dir>/scripts/deploy/deb.py <target> push ./foo_1.0_amd64.deb --yes
# 仅上传到远程暂存目录(默认 {base_path}/.debs;裸主机为 /tmp/deployer-debs)
python3 <skill-dir>/scripts/deploy/deb.py <target> scp ./foo_1.0_amd64.deb
# 安装该节点暂存目录里已上传的全部 .deb
python3 <skill-dir>/scripts/deploy/deb.py <target> dpkg --yes
# 远程直接从 URL 下载安装
python3 <skill-dir>/scripts/deploy/deb.py <target> apt https://example.com/foo_1.0_amd64.deb --yes
```
- 非 root 用户走 `sudo -n`(需免密 sudo);`--yes` 传 `-y`。没配 sudo 免密时会直接失败。
- 升级同版本号前可 `ssh <node> "dpkg -I <暂存路径>"`;装完验证:`ssh <node> "dpkg -l | grep <pkg>"`。
## NAS / rsync 失败
部分 NAS 的 SSH 用户禁用 rsync 协议(Permission denied)。表现是 sync 报错但 ssh 正常。处理顺序:
1. 该节点 `_config.yaml` 写真实 `base_path`(如 `/volume1/docker`,避开符号链接路径)
2. 仍失败则手动 tar over SSH:
```bash
tar czf - -C <服务目录> . --exclude='data' --exclude='_data' \
| ssh <node> "mkdir -p <base_path>/<name> && cd <base_path>/<name> && tar xzf -"
ssh <node> "cd <base_path>/<name> && /usr/local/bin/docker compose up -d"
```
tar 不会删除远程多余文件;需清理旧文件时手动 SSH 删除。Synology 上 docker 路径可能是 `/usr/local/bin/docker`。
## 验证
- `list.py` 输出全部服务与节点分布,数量与预期一致
- 每次 sync/deploy 后 `remote.py <svc> ps` 容器 Up、`logs` 无报错
- 升级后额外确认镜像 tag 与 `compose.yaml` 一致
- 改 Traefik/Caddy 路由后 curl 对应域名验证生效
@@ -2,7 +2,7 @@
`_config.yaml` 供 **Compose 轨道**脚本解析,决定同步目标与排除规则。可放在**服务目录、部署根或其任意祖先目录**;子目录中的字段覆盖父目录(继承合并)。
Argo CD 轨道用独立文件 `.skiff/deployer/argocd.yaml`,字段见 [argocd.md](argocd.md)。
Argo CD 轨道用独立文件 `.pouch/deployer/argocd.yaml`,字段见 [argocd.md](argocd.md)。
不要把 `argocd:` 嵌进本文件(脚本解析器不支持嵌套映射)。
## 放置位置(两种布局)
@@ -10,16 +10,16 @@ Argo CD 轨道用独立文件 `.skiff/deployer/argocd.yaml`,字段见 [argocd.
| 布局 | 部署根 | `_config.yaml` 典型位置 |
|------|--------|------------------------|
| 独立配置中心仓库(`DEPLOYER_ROOT` 指向) | 仓库根 | 主机目录 `vyyo1/_config.yaml`、服务目录 |
| 项目内环境 `.skiff/deployer/{env}/` | `.skiff/deployer/` | 根级共享默认、各环境目录覆盖 |
| 项目内环境 `.pouch/deployer/{env}/` | `.pouch/deployer/` | 根级共享默认、各环境目录覆盖 |
项目布局示例:
```yaml
# .skiff/deployer/_config.yaml — 三个环境共享
# .pouch/deployer/_config.yaml — 三个环境共享
node: my-vps
base_path: /srv/apps
# .skiff/deployer/prod/_config.yaml — 仅生产环境差异
# .pouch/deployer/prod/_config.yaml — 仅生产环境差异
node: prod-vps # 覆盖父级
name: my-project-api # 可选,覆盖默认的 {项目名}-{env}
```
@@ -111,7 +111,7 @@ sync_exclude:
| 同一 SSH 主机多个服务 | 主机目录写一份 `node`/`base_path`,子服务免重复 |
| 各服务目标不同 | 服务目录单独写 `_config.yaml` |
| 个别覆盖 | 子目录只写差异字段 |
| 项目三环境同主机 | `.skiff/deployer/_config.yaml` 写共享 node,各环境只放差异 |
| 项目三环境同主机 | `.pouch/deployer/_config.yaml` 写共享 node,各环境只放差异 |
| 项目环境分属不同主机 | 各环境 `_config.yaml` 分别写 `node` |
## 示例
@@ -137,4 +137,4 @@ node: deploy@web2-backup
- 目录内有 `compose.yaml`
- 能通过继承或兜底解析出 `node`
- 独立仓库布局:路径中不含 `unused/` 段
- 项目环境布局:位于部署根 `.skiff/deployer/` 内(其外的项目文件不扫描)
- 项目环境布局:位于部署根 `.pouch/deployer/` 内(其外的项目文件不扫描)
+285
View File
@@ -0,0 +1,285 @@
#!/usr/bin/env python3
"""Readiness checker for deployer project layout.
Usage:
python3 -I -S check.py [--project DIR]
Resolves the deploy root as DEPLOYER_ROOT, else <project>/.pouch/deployer
(or .skiff/deployer), else walking up from cwd. Does not SSH, rsync, or
start containers. Exit 0 = PASS (SKIP allowed), 1 = FAIL, 2 = usage.
"""
from __future__ import annotations
import argparse
import os
import re
import shutil
import sys
from pathlib import Path
_SCRIPT_DIR = Path(__file__).resolve().parent
if str(_SCRIPT_DIR) not in sys.path:
sys.path.insert(0, str(_SCRIPT_DIR))
import lib # noqa: E402
PASS = "PASS"
FAIL = "FAIL"
SKIP = "SKIP"
_PROJECT_LAYOUT_DIRS = (".pouch", ".skiff")
_REPO_OR_DIR = re.compile(r"^(repo|repo_dir)\s*:", re.MULTILINE)
class Report:
def __init__(self) -> None:
self.failures = 0
def add(self, status: str, number: int, title: str, detail: str) -> None:
print(f"[{status}] {number}. {title}")
for line in detail.splitlines():
print(f" {line}")
if status == FAIL:
self.failures += 1
def node_in_ssh_config(node: str, hosts: set[str]) -> bool:
if node in hosts:
return True
if "@" in node:
_, host = node.rsplit("@", 1)
return host in hosts
return False
def resolve_deploy_root(project: Path | None) -> tuple[Path | None, str]:
env = os.environ.get("DEPLOYER_ROOT", "").strip()
if env:
path = Path(env).expanduser().resolve()
return (path if path.is_dir() else None), "DEPLOYER_ROOT"
if project is not None:
root = project.resolve()
for dirname in _PROJECT_LAYOUT_DIRS:
candidate = root / dirname / "deployer"
if candidate.is_dir():
return candidate, "project"
return None, "project"
found = lib._find_project_root()
if found.name == "deployer" and found.parent.name in _PROJECT_LAYOUT_DIRS:
return found, "project"
if found == lib._SKILL_DIR.parent:
return None, "cwd"
return found, "cwd"
def compose_service_dirs(root: Path) -> list[Path]:
dirs: list[Path] = []
for compose in sorted(root.glob("**/compose.yaml")):
service_dir = compose.parent
if not lib.is_deployable_dir(service_dir, root):
continue
dirs.append(service_dir)
return dirs
def check_layout(report: Report, root: Path | None, source: str, project: Path | None) -> bool:
if root is None:
if source == "DEPLOYER_ROOT":
report.add(
FAIL,
1,
"部署根存在",
"DEPLOYER_ROOT is set but is not a directory",
)
else:
hint_root = project.resolve() if project is not None else Path.cwd()
report.add(
FAIL,
1,
"部署根存在",
"\n".join(
[
f"no .pouch/deployer under {hint_root}",
"Fix: run deployer 初始化 and create .pouch/deployer/",
" _config.yaml # node: <ssh-host-alias>",
" test/compose.yaml # default env for ACK",
]
),
)
return False
kind = "project layout" if lib.in_project_layout(root) else "standalone deploy root"
report.add(PASS, 1, "部署根存在", f"{root} ({kind}, via {source})")
return True
def check_toolchain(report: Report) -> None:
ssh_ok = shutil.which("ssh") is not None
rsync_ok = shutil.which("rsync") is not None
lines = [
f"ssh: {'found' if ssh_ok else 'MISSING (blocks compose deploy)'}",
f"rsync: {'found' if rsync_ok else 'MISSING (scp fallback)'}",
]
if not ssh_ok:
lines.append("install openssh-client")
report.add(FAIL, 2, "工具链", "\n".join(lines))
return
report.add(PASS if rsync_ok else SKIP, 2, "工具链", "\n".join(lines))
def check_argocd(report: Report, root: Path) -> bool:
path = root / "argocd.yaml"
if not path.is_file():
report.add(SKIP, 3, "Argo CD 指针", "no argocd.yaml")
return False
text = path.read_text(encoding="utf-8")
if _REPO_OR_DIR.search(text):
report.add(PASS, 3, "Argo CD 指针", "argocd.yaml has repo or repo_dir")
return True
report.add(
FAIL,
3,
"Argo CD 指针",
"argocd.yaml exists but has neither repo: nor repo_dir:\n"
"Fix: repo: git@host:org/infra-gitops.git",
)
return True
def check_services(report: Report, root: Path) -> None:
services = compose_service_dirs(root)
if not services:
report.add(
FAIL,
4,
"至少有一个 compose.yaml",
"no compose.yaml under the deploy root\n"
"Fix: add .pouch/deployer/<env>/compose.yaml (env usually test)",
)
report.add(SKIP, 5, "每个服务能解析 node", "(no compose.yaml)")
report.add(SKIP, 6, "node 出现在 SSH config", "(no compose.yaml)")
report.add(SKIP, 7, "list 可发现服务", "(no compose.yaml)")
return
rels = [str(path.relative_to(root)) for path in services]
report.add(PASS, 4, "至少有一个 compose.yaml", "\n".join(rels))
hosts = lib.ssh_config_hosts()
node_lines = []
ssh_lines = []
node_fail = False
ssh_fail = False
for path in services:
rel = str(path.relative_to(root))
info = lib.service_info(rel, strict=False)
if info is None:
node_lines.append(f"{rel}: MISSING node")
ssh_lines.append(f"{rel}: skipped (no node)")
node_fail = True
continue
node = str(info["node"])
node_lines.append(f"{rel}: node={node}")
if node_in_ssh_config(node, hosts):
ssh_lines.append(f"{rel}: {node} in ~/.ssh/config")
else:
ssh_lines.append(f"{rel}: {node} NOT in ~/.ssh/config")
ssh_fail = True
if node_fail:
node_lines.extend(
[
"",
"Fix: write node in _config.yaml (deploy root or env dir).",
"Example:",
" node: my-vps",
" base_path: /opt/app",
"Do not invent a hostname. It must be an SSH Host alias.",
]
)
report.add(FAIL, 5, "每个服务能解析 node", "\n".join(node_lines))
else:
report.add(PASS, 5, "每个服务能解析 node", "\n".join(node_lines))
if ssh_fail or node_fail:
if ssh_fail:
ssh_lines.extend(
[
"",
"Fix: add a Host entry to ~/.ssh/config for the node alias.",
"This check does not open an SSH connection.",
]
)
report.add(FAIL, 6, "node 出现在 SSH config", "\n".join(ssh_lines))
else:
report.add(PASS, 6, "node 出现在 SSH config", "\n".join(ssh_lines))
found = lib.discover_services()
if not found:
report.add(
FAIL,
7,
"list 可发现服务",
"compose.yaml exists but discover_services found none "
"(need resolvable node)",
)
return
names = [os.path.relpath(item["service_dir"], root) for item in found]
report.add(PASS, 7, "list 可发现服务", f"{len(found)} service(s): " + ", ".join(names))
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
)
parser.add_argument(
"--project",
type=Path,
default=None,
help="project root (looks for .pouch/deployer); ignored when DEPLOYER_ROOT is set",
)
args = parser.parse_args(argv)
report = Report()
project = args.project.resolve() if args.project is not None else None
root, source = resolve_deploy_root(project)
layout_ok = check_layout(report, root, source, project)
check_toolchain(report)
if not layout_ok:
report.add(SKIP, 3, "Argo CD 指针", "(no deploy root)")
report.add(SKIP, 4, "至少有一个 compose.yaml", "(no deploy root)")
report.add(SKIP, 5, "每个服务能解析 node", "(no deploy root)")
report.add(SKIP, 6, "node 出现在 SSH config", "(no deploy root)")
report.add(SKIP, 7, "list 可发现服务", "(no deploy root)")
print()
print(f"RESULT: FAILED ({report.failures} check(s) failed)")
return 1
assert root is not None
lib.PROJECT_ROOT = root
lib._SSH_HOSTS = None
previous_cwd = Path.cwd()
try:
os.chdir(root)
has_argocd = check_argocd(report, root)
services = compose_service_dirs(root)
if services:
check_services(report, root)
elif has_argocd:
report.add(SKIP, 4, "至少有一个 compose.yaml", "Argo CD only; no compose env")
report.add(SKIP, 5, "每个服务能解析 node", "Argo CD only")
report.add(SKIP, 6, "node 出现在 SSH config", "Argo CD only")
report.add(SKIP, 7, "list 可发现服务", "Argo CD only")
else:
check_services(report, root)
finally:
os.chdir(previous_cwd)
print()
if report.failures:
print(f"RESULT: FAILED ({report.failures} check(s) failed)")
return 1
print("RESULT: PASSED")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+5 -1
View File
@@ -31,7 +31,11 @@ import subprocess
import sys
from pathlib import Path
from lib import (
_SCRIPT_DIR = Path(__file__).resolve().parent
if str(_SCRIPT_DIR) not in sys.path:
sys.path.insert(0, str(_SCRIPT_DIR))
from lib import ( # noqa: E402
project_root,
run_remote,
scp_base_args,
+12 -10
View File
@@ -3,7 +3,7 @@
支持两种布局:
1. 独立配置中心仓库(DEPLOYER_ROOT 指向,或 skill 安装位置)
2. 项目内环境目录 .skiff/deployer/{prod,test,dev}/(从 CWD 自动发现)
2. 项目内环境目录 .pouch/deployer/{prod,test,dev}/(从 CWD 自动发现;兼容 .skiff/deployer)
"""
from __future__ import annotations
@@ -18,10 +18,11 @@ DEFAULT_BASE_PATH = "/opt/app"
DEFAULT_SYNC_EXCLUDES = ("data", "_data")
_SKILL_DIR = Path(__file__).resolve().parent.parent # scripts/
PROJECT_ROOT: Path | None = None
_PROJECT_LAYOUT_DIRS = (".pouch", ".skiff")
def _find_project_root() -> Path:
"""部署根:DEPLOYER_ROOT > 从 CWD 向上找 .skiff/deployer > skill 安装位置。"""
"""部署根:DEPLOYER_ROOT > 从 CWD 向上找 .pouch/deployer > skill 安装位置。"""
env = os.environ.get("DEPLOYER_ROOT", "").strip()
if env:
p = Path(env).expanduser().resolve()
@@ -31,9 +32,10 @@ def _find_project_root() -> Path:
return p
cur = Path.cwd()
while True:
cand = cur / ".skiff" / "deployer"
if cand.is_dir():
return cand
for dirname in _PROJECT_LAYOUT_DIRS:
cand = cur / dirname / "deployer"
if cand.is_dir():
return cand
if cur == cur.parent:
break
cur = cur.parent
@@ -48,13 +50,13 @@ def project_root() -> Path:
def in_project_layout(root: Path | None = None) -> bool:
"""部署根是否为某项目内的 .skiff/deployer/。"""
"""部署根是否为某项目内的 .pouch/deployer/。"""
root = root or project_root()
return root.name == "deployer" and root.parent.name == ".skiff"
return root.name == "deployer" and root.parent.name in _PROJECT_LAYOUT_DIRS
def project_display_name(root: Path | None = None) -> str:
"""项目名:git 仓库名优先,否则 .skiff 的父目录名。"""
"""项目名:git 仓库名优先,否则 .pouch 的父目录名。"""
root = root or project_root()
anchor = root.parent.parent if in_project_layout(root) else root
try:
@@ -111,7 +113,7 @@ def load_config(config_path: os.PathLike | str, *, required: bool = True) -> dic
def config_paths_for_service(service_dir: str) -> list[Path]:
"""收集部署根自身及服务目录各层 _config.yaml(祖先在前,服务目录在后)。
部署根的 _config.yaml(如 .skiff/deployer/_config.yaml)作为全局默认,
部署根的 _config.yaml(如 .pouch/deployer/_config.yaml)作为全局默认,
对所有环境/服务生效。
"""
root = project_root()
@@ -333,7 +335,7 @@ def service_info(service_dir: str, *, strict: bool = True) -> dict | None:
def is_deployable_dir(path: Path, root: Path) -> bool:
if "unused" in path.parts or "__pycache__" in path.parts:
return False
if ".skiff" in path.parts and root.name != "deployer":
if any(part in _PROJECT_LAYOUT_DIRS for part in path.parts) and root.name != "deployer":
return False
if not (path / "compose.yaml").is_file():
return False
+6 -1
View File
@@ -6,8 +6,13 @@ from __future__ import annotations
import os
import sys
from collections import defaultdict
from pathlib import Path
from lib import discover_services, project_root
_SCRIPT_DIR = Path(__file__).resolve().parent
if str(_SCRIPT_DIR) not in sys.path:
sys.path.insert(0, str(_SCRIPT_DIR))
from lib import discover_services, project_root # noqa: E402
def main() -> int:
+6 -1
View File
@@ -20,8 +20,13 @@ import argparse
import os
import subprocess
import sys
from pathlib import Path
from lib import project_root, run_remote, service_info
_SCRIPT_DIR = Path(__file__).resolve().parent
if str(_SCRIPT_DIR) not in sys.path:
sys.path.insert(0, str(_SCRIPT_DIR))
from lib import project_root, run_remote, service_info # noqa: E402
REMOTE_COMMANDS = {
"up": "docker compose up -d",
+71 -27
View File
@@ -16,15 +16,20 @@ import shlex
import shutil
import subprocess
import sys
from pathlib import Path
from lib import DEFAULT_BASE_PATH, project_root, rsync_ssh_args, service_info, ssh_cmd
_SCRIPT_DIR = Path(__file__).resolve().parent
if str(_SCRIPT_DIR) not in sys.path:
sys.path.insert(0, str(_SCRIPT_DIR))
from lib import DEFAULT_BASE_PATH, rsync_ssh_args, scp_base_args, service_info, ssh_cmd # noqa: E402
def sync_directory(info: dict) -> int:
if shutil.which("rsync"):
return _sync_rsync(info)
print("本机没有 rsync,改用 tar over SSH(不会删除远程多余文件)")
return _sync_tar(info)
print("本机没有 rsync,改用 scp(不会删除远程多余文件)")
return _sync_scp(info)
def _sync_rsync(info: dict) -> int:
@@ -58,42 +63,81 @@ def _sync_rsync(info: dict) -> int:
print(f"错误: rsync 执行失败,退出码: {e.returncode}")
return 1
except FileNotFoundError:
print("错误: rsync 命令未找到,请确保已安装 rsync")
return 1
print("本机没有 rsync,改用 scp(不会删除远程多余文件)")
return _sync_scp(info)
def _sync_tar(info: dict) -> int:
def collect_sync_files(source_dir: str, excludes: list[str]) -> list[Path]:
"""列出待同步文件;排除规则与 rsync `--exclude=name/` 一致(任意层级的同名目录)。"""
source = Path(source_dir)
exclude_set = set(excludes)
files: list[Path] = []
for root, dirs, names in os.walk(source):
dirs[:] = [d for d in dirs if d not in exclude_set]
for name in names:
path = Path(root) / name
if path.is_file():
files.append(path)
return files
def _sync_scp(info: dict) -> int:
source_dir = info["service_dir"]
remote_dir = info["remote_dir"]
tar_cmd = ["tar", "czf", "-", "-C", source_dir]
for item in info["sync_exclude"]:
tar_cmd.append(f"--exclude={item}")
tar_cmd.append(".")
node = info["node"]
source = Path(source_dir)
files = collect_sync_files(source_dir, info["sync_exclude"])
remote_shell = (
f"mkdir -p {shlex.quote(remote_dir)} && "
f"tar xzf - -C {shlex.quote(remote_dir)}"
)
ssh = ssh_cmd(info, remote_shell)
print(f"正在同步 {source_dir} 到 {info['node']}:{remote_dir}")
print(f"正在同步 {source_dir} 到 {node}:{remote_dir}")
if info["sync_exclude"]:
print(f"排除目录: {', '.join(info['sync_exclude'])}")
print(f"执行命令: tar | {' '.join(ssh)}")
print("-" * 60)
tar = subprocess.Popen(tar_cmd, stdout=subprocess.PIPE)
remote_dirs = {remote_dir}
grouped: dict[str, list[Path]] = {}
for path in files:
parent = path.relative_to(source).parent.as_posix()
grouped.setdefault(parent, []).append(path)
if parent != ".":
remote_dirs.add(f"{remote_dir}/{parent}")
mkdir_remote = "mkdir -p " + " ".join(shlex.quote(d) for d in sorted(remote_dirs))
mkdir_cmd = ssh_cmd(info, mkdir_remote)
print(f"执行命令: {' '.join(mkdir_cmd)}")
try:
completed = subprocess.run(ssh, stdin=tar.stdout, check=False)
finally:
if tar.stdout:
tar.stdout.close()
tar.wait()
if tar.returncode:
print(f"错误: tar 打包失败,退出码: {tar.returncode}")
return tar.returncode
completed = subprocess.run(mkdir_cmd, check=False)
except FileNotFoundError:
print("错误: ssh 命令未找到")
return 1
if completed.returncode:
print(f"错误: 远程 tar 解包失败,退出码: {completed.returncode}")
print(f"错误: 远程 mkdir 失败,退出码: {completed.returncode}")
return completed.returncode
if not files:
print("没有需要同步的文件")
print("-" * 60)
print("同步完成!")
return 0
try:
for parent, group in sorted(grouped.items()):
dest = remote_dir if parent == "." else f"{remote_dir}/{parent}"
scp_cmd = [
*scp_base_args(info),
"-p",
"--",
*[str(p) for p in group],
f"{node}:{dest}/",
]
print(f"执行命令: {' '.join(scp_cmd)}")
subprocess.run(scp_cmd, check=True)
except subprocess.CalledProcessError as e:
print(f"错误: scp 执行失败,退出码: {e.returncode}")
return 1
except FileNotFoundError:
print("错误: scp 命令未找到,请确保已安装 openssh-client")
return 1
print("-" * 60)
print("同步完成!")
return 0
+57
View File
@@ -0,0 +1,57 @@
# discussion-notes
边讨论边把可复用结论写进项目里的 Markdown 笔记。笔记是活的:理解变了就改,不另存聊天稿。
## 什么时候使用
- 要进入讨论模式、沉淀到文档、整理笔记、单开文档。
- 适合学习概念、做技术选型、把讨论变成可回顾的结论。
- 不适合:一次性问答、必须进 skill 记忆库的沉淀(用 `memory-distill`)、也不要用来当会议逐字稿。
说「结束讨论」或「不用记了」就退出;之后不再自动改笔记。
## 安装
```bash
pouch add discussion-notes # 当前项目
pouch add discussion-notes -g # 全局
```
## 使用前准备
- 指定笔记路径,或接受默认 `000inbox/<topic>.md`(按 Obsidian 收件箱习惯)。
- 有明确话题名更好;没有的话 Agent 会问一次。
- 笔记写在当前项目仓库内。默认不 git commit。
## 使用示例
```text
进入讨论模式,记到 000inbox/podman-vs-docker.md:公司 Debian VM 上容器运行时选 Podman 还是 Docker。
```
```text
讨论 Quadlet 是什么,单开一篇概念笔记,并链到刚才的选型文档。
```
```text
这轮不用记,先只回答。
```
```text
结束讨论。
```
## Agent 会做什么
1. 确认路径:你指定的文件,或默认 `000inbox/<topic>.md`。新话题新建,续聊改已有文件。
2. 按内容选结构:选型/决策,或概念/原理。模板见 [reference.md](reference.md)。
3. 每轮先用中文回答你,再更新笔记里的结论、对比、约束;纠正旧理解时直接改,不留错误版本。
4. 只写可复用知识,不把问答全文抄进文档,不创建 `.raw.md`。
5. 你说结束时,可选加一节「更新」:搞懂了、纠正了、待动手。
## 如何判断完成
- 约定路径上有一份正在维护的 `.md`,结构能看出结论而不是聊天记录。
- 本轮新结论已经写进对应章节;过时说法被改掉,而不是追加在文末。
- 没有出现 `xxx.raw.md`,也没有未经确认的 git commit。
- 退出讨论后,Agent 不再自动改那份笔记。
+8 -1
View File
@@ -8,6 +8,13 @@
- 已有 `.drawio` 原型,希望补齐跳转、异常态、权限或规则说明。
- 需要一张能用于产品评审、开发对齐和测试设计的产品说明板。
## 安装
```bash
pouch add draw-prototype-flow # 当前项目
pouch add draw-prototype-flow -g # 全局
```
## 使用前准备
最好提供产品目标、目标用户、核心任务和已有需求材料。缺少细节时,Agent 会先列出必要假设;只有关键范围无法判断时才会询问。
@@ -34,4 +41,4 @@
## 如何判断完成
最终会得到可在 diagrams.net/Draw.io 打开的 `.drawio` 源文件。核心任务应能从入口走到结果,每个关键按钮有明确去向,异常状态有恢复动作,原型、动线和规格使用同一套页面与状态 ID。
最终会得到可在 diagrams.net/Draw.io 打开的 `.drawio` 源文件。你指定路径就用指定路径,没指定时 Agent 会在项目里给出文件名。核心任务应能从入口走到结果,每个关键按钮有明确去向,异常状态有恢复动作,原型、动线和规格使用同一套页面与状态 ID。
+53
View File
@@ -0,0 +1,53 @@
# eli5
用大图少字的单页 HTML,把一个话题讲给完全没背景的人听。
改编自 [anthropics/claude-plugins-community](https://github.com/anthropics/claude-plugins-community) 的 `eli5`。
## 什么时候使用
- `/eli5 DNS 怎么工作`
- 「像五岁小孩一样解释 RAG」
- 「画图讲一下这个项目的登录流程」
- 想要一份能转发的图解,而不是聊天长文
不适合:核对精确规格、安全审计、修代码。那类工作先用 eli5 抓大图,再换别的 skill 或原文档抠细节。
## 使用前准备
- 给出话题。可以是概念(DNS、RAG),也可以是当前项目里的一块(登录、发版)。
- 想指定输出路径就说一声;不说则写到项目根目录的 `eli5-<话题>.html`。
## 安装
```bash
pouch add eli5 # 当前项目
pouch add eli5 -g # 全局
```
## 使用示例
```text
/eli5 DNS 怎么工作
```
```text
像五岁小孩一样解释这个仓库的认证是怎么走的。
```
```text
eli5 RAG,输出到 docs/eli5-rag.html
```
## Agent 会做什么
1. 从你的话里取出话题。
2. 按「读者完全没背景」来讲,而不是装成对幼儿说话。
3. 写一页自包含 HTML:大图(inline SVG)、极少文字、至少 3 个可见步骤。
4. 把文件写到约定路径;聊天里只回路径和一句总收束。
## 如何判断完成
- 能用浏览器直接打开那份 HTML,不依赖外网资源。
- 画面上图大于字;没背景的人能看出「这东西怎么走」。
- 聊天没有再贴一遍长文解释。
+52
View File
@@ -0,0 +1,52 @@
---
name: eli5
description: >-
用大图少字的单页 HTML 把话题讲给完全没背景的人听。Use when the user types
/eli5 <topic>, says eli5, 像五岁小孩一样解释, 用小学生能懂的方式讲, 画图解释,
or asks for a dead-simple picture explainer of how something works.
---
# eli5
把话题讲给**完全没背景**的人听。交付物是一页自包含 HTML:大图、少字,不是聊天长文。
改编自 [anthropics/claude-plugins-community](https://github.com/anthropics/claude-plugins-community) 的 `eli5`(Thariq Shihipar)。原技能用 `$ARGUMENTS`;这里从用户请求里取话题。
---
## 何时使用
- `/eli5 <topic>`、`eli5`、像五岁小孩一样解释、用小学生能懂的方式讲、画图解释
- 用户要一份一眼能看懂的图解,而不是长文
## 不适用
- 精确规格、边界条件、安全审计、修代码
- 高保真 UI 或可编辑原型 → `draw-prototype-flow`
- 边讨论边写笔记 → `discussion-notes`
---
## 步骤
1. **取话题**。用户没给就问一次。
2. **读者设定**:零背景,不是幼儿口吻。去掉默认假设,不要装小孩说话。
3. **写一份自包含 HTML**(无外部 CSS / JS / 字体 / 图片 URL):
- 一页看完
- 结构用大图讲(inline SVG;可少量 emoji)
- 文字极短:标题、箭头旁短语、一句总收束
- 至少把核心机制拆成 3 个可见步骤或块
4. **落盘**。用户指定路径就用;否则写到当前项目根(没有项目就用 cwd)`eli5-<slug>.html`。Claude Code 若支持 artifact,可同时打开,**仍要写文件**。
5. **聊天里只给路径和一句总收束**,不要把正文再讲一遍。
## 注意事项
- 不为了好懂而编造机制。不确定就标「简化」,不要装精确。
- 不 git commit。
- 语言跟用户请求走;没点名时用简体中文。
## 验证
- 用浏览器或 `file://` 打开是单页,图明显大于字。
- 没背景的人能从画面看出「这东西怎么走」。
- 仓库或 cwd 里有那份 HTML;聊天没有墙一样的解释。
+43
View File
@@ -0,0 +1,43 @@
# fixds
把 DeepSeek Flash 系模型容易犯的「浪费时间」动作(先实现后验证、无边界扫描文件、
越权改动、命令反复写错、测试跑超范围)变成一份开工前必须对照的硬规则。
## 什么时候使用
- 你时间紧、只想快拿到结果,希望它别把轮次花在无关探索上。
- 你正在用 DeepSeek Flash / DeepSeek V4.1 Flash 之类的模型,希望它先验证再动手、
不乱扫文件、不擅自改你的要求或别的仓库。
- 你想手动给当前会话加上这套约束:直接说 `fixds` 或「按 fixds 规则来」。
- 适用于任何项目;与具体技术栈无关。
## 使用前准备
- 无需配置。它面向 DeepSeek Flash / DeepSeek V4.1 Flash;其它模型命中时同样按这套
规则做,不会额外增加流程。
## 使用示例
```text
fixds:这轮按约束做,先把要跑的验证命令给我看,再动代码。
```
```text
用 fixds 检查一下你刚才的做法,有没有无边界搜索或越权改动。
```
## Agent 会做什么
1. 开工前回答四个问题:命令范围、是否已验证语义、是否与要求一致、路径是否属于本项目。
2. 把每个动作、每次追问、每段输出都压在「能缩短拿到结果的时间」上:先给结论和证据、
独立命令一起发、能自查的不问你、同一事实不重复、无关线索不展开。
3. 按七条硬规则执行:每轮都逼近结论、先验证再实现、搜索有边界、只改被要求的范围、
命令一次写对、测试按影响面、冲突时报告并等待确认而不是自行改需求。
4. 交付前自检:需求一致性、改动范围、验证证据。
## 如何判断完成
- 你的要求被原样执行,或偏差以「你的要求 / 实测结果 / 影响 / 选项」的形式先摆给你决定,
而不是已经改掉。
- 改动的文件都在你点名的范围内;项目外目录的改动都先问过你。
- 给出的验证命令与输出可以直接复现。
+100
View File
@@ -0,0 +1,100 @@
---
name: fixds
description: >-
DeepSeek Flash 系模型的行为约束:先验证再实现、搜索必须有边界、不越权改需求
或项目外目录、命令一次写对、测试按影响面;用户赶时间时每轮都要逼近结论。
用于 DeepSeek Flash / DeepSeek V4.1 Flash(模型标识含 deepseek 与 flash);
用户说 fixds 时同样生效。
---
# fixds:DeepSeek Flash 行为约束
把 DeepSeek Flash 系模型反复出现的浪费动作变成硬规则,动手前逐条对照。
本 skill 面向 DeepSeek Flash / DeepSeek V4.1 Flash(命中即按下面规则执行,不必再
判定模型是否匹配);用户点名 fixds 时同样生效。
---
## 开工前 4 问(fail-closed)
任一答不上来,先解决再动手:
| # | 问 | 不满意时 |
| --- | --- | --- |
| 1 | 这条命令的**范围**多大?会扫到会话日志、缓存、`node_modules` 吗? | 换精确路径、`git ls-files`、或已知目录 |
| 2 | 我**先验证**过语义了吗(格式、排序、协议、兼容性)? | 先用一条最小命令验证,再写代码与文档 |
| 3 | 这**跟用户明确要求一致**吗? | 不一致就照要求做;确有冲突则报告 + 等确认 |
| 4 | 这个路径**属于当前项目**吗? | 属于别的仓库或用户目录:先判断必要性,再询问 |
---
## 硬规则
### 1. 用户在赶时间:每轮都要逼近结论
用户默认在赶时间。每个动作、每次追问、每段输出都要能缩短拿到结果的时间。
- 先给结论和证据,再给必要背景;不要把探索过程、失败尝试、思考过程写进回复。
- 互不依赖的命令一次发出,不要一条一条试。
- 能自己查证的不问用户;需要用户拍板的取舍,一次把选项列全,不要挤牙膏式追问。
- 同一事实只查一次、只说一次;已确认的不重复验证或复述。
- 与结论无关的线索(无关测试失败、旁支问题、「顺便更全面一点」)先放下,汇报里一句话带过。
- 同一路径连续两轮无产出就换方法,或直接问用户。
- 省掉的是多余动作和多余追问,不是必要确认:要求与实测冲突、越界改动、破坏性操作仍须先问。
### 2. 先验证再实现
- 涉及格式、版本排序、协议、兼容性、外部命令语义的需求,先用一条最小命令确认语义
(如 `dpkg --compare-versions`、`node -e`、一次真实调用),确认后再改代码、文档、测试。
- 禁止「脚本 + README + 契约 + 测试全写完,才去验证假设」。假设一旦被推翻,全部返工。
- 用户给的格式、常量、命名**照做**,即使验证显示它不理想:保留用户要求,同时报告实测
现象与选项,等确认后再动。
### 3. 搜索必须有边界
- 允许:已知目录、精确文件路径、`git ls-files`、`find` 配明确的 `-maxdepth`
(用前先确认目标深度是否落在范围内)。
- 禁止:`grep -r` / `find` 扫 `$HOME`、`/` 或任何含会话记录、缓存、`node_modules` 的大目录。
- 同类命令连续两次失败或被打断 → 停止重试,换方法或直接问用户。
### 4. 只在被要求的范围内动作
- 只改用户点名的仓库与文件。同步副本、清理残留、找「别处是否还有一份」之前先问。
- 操作当前项目之外的目录:先判断是否必要,确有必要**先询问授权**,不要顺手做。
- 提交、推送、安装、发布、改配置范围:只在用户明确要求时执行。
### 5. 命令一次写对
- 时间戳、排序方向、路径深度、正则边界:先算一次、先看输出,再写进脚本或断言。
- 注释与说明文字不要塞进参数列表或字符串(会破坏语法)。
- 脚本先小样本跑通再放大;结果与预期不符,先怀疑自己的参数、方向、标签,而不是结论。
### 6. 测试与排查按影响面
- 只跑受影响目录的测试;无关目录的失败不追(可按 diff 路径判定无关),只报告。
- 命令报错先确认不是自己造成的(参数、路径、语法),不要重复执行同一条失败命令。
### 7. 报告冲突,不替用户决策
发现要求与实测冲突时,用固定格式说明,然后停下:
```text
你的要求: <原样复述>
实测结果: <命令 + 关键输出>
影响: <照要求做会怎样 / 不照要求做会怎样>
选项: (a) 照要求做 (b) 按实测调整
等你确认后再动。
```
---
## 交付前自检
1. 需求一致性:与用户明确要求的格式、命名、范围逐条核对,有偏差点已报告而非已改掉。
2. 范围:改动只落在被点名的仓库/文件;项目外目录的改动都有授权记录。
3. 证据:验证命令与输出已给出,不是「应该没问题」。
## 参考
- 每条规则对应的真实案例、代价与正确做法:读
[references/anti-patterns.md](references/anti-patterns.md)。
+56
View File
@@ -0,0 +1,56 @@
# 浪费案例与正确做法
本 skill 的来源是一次真实会话的复盘,判定标准是「该动作对结论有没有贡献,能不能避免」,
以及用户在等结果上付出的时间。规则正文在 `SKILL.md`,本文件解释每条规则为什么存在。
## 案例 1:先实现后验证(代价最高)
- 多余动作:用户指定了新格式 `20260918T1722`,于是把生成脚本、README、契约文档、测试
全部按该格式改完,之后才去验证排序语义。
- 结果:验证发现该格式在同一天会小于旧格式包(Debian 把字母排在标点之前),同一批文件
全部重写一遍。
- 正确做法:拿到格式先用一条 `dpkg --compare-versions` 比对真实旧包与候选格式,确认排序
语义成立,再动手写代码、文档与测试。一条命令的成本 vs 整套返工。
- 附带教训:验证结果只用于「向用户报告并给选项」,不代表可以替用户改需求。
## 案例 2:无边界扫描
- 多余动作:`grep -rln <模式> /home/<user>`、`find /home/<user> -maxdepth N` 用来找分支、
查找第三份脚本副本。
- 结果:连续三次因输出过大或命中会话日志被中断,零有效产出,还占掉多轮交互。
- 正确做法:搜索限定在已知代码目录、精确路径或 `git ls-files`;`find` 先确认目标深度。
用户家目录下的会话记录、缓存、`node_modules` 不该进入扫描范围。
## 案例 3:根因已定后继续追无关线索
- 多余动作:根因(版本精度不足 + 字符串排序)已确认后,逐仓库执行 `git branch -a`、
翻 `packed-refs`、查看多个仓库,试图找出「是哪个项目产出的包」。
- 结果:对结论零贡献,多轮浪费。
- 正确做法:定位生成脚本(一次精确搜索即可)≠ 定位项目。需要项目信息时直接问用户,
或只在用户指明的目录里找。
## 案例 4:主动扩大改动范围
- 多余动作:用户只要求修本仓库,却顺手把另一仓库里的 vendored 副本一起改了,还想继续
找「是否还有第三份副本」。
- 结果:用户明确质疑越权;改动随后被撤销。
- 正确做法:改本项目之外的目录前,先判断必要性并向用户确认;用户没点名的仓库不动。
## 案例 5:自造的返工
- 排序脚本方向写反、把说明性注释写进版本号字符串导致工具报语法错误、把注释插入函数调用
参数中间导致语法错误、时间戳算错(当地 10:00 记成 02:00)、断言方向写反。
- 每一项都多花一轮重跑。
- 正确做法:算完先打印一次确认;注释放在语句之外;小样本先跑通;结果异常先怀疑自己的
输入,而不是先怀疑结论。
## 案例 6:测试范围失控
- 多余动作:只改了某个子目录,却跑了全量测试(数百个),随后排查其中几个与本改动无关的
失败(失败测试只读取另一个未改动的目录)。
- 正确做法:按 diff 影响面选择测试范围;无关失败只在汇报里说明,不投入排查轮次。
## 使用方式
开工前过一遍 `SKILL.md` 的 4 问;交付前过一遍自检。发现新的浪费模式时,把案例补进本文件,
并检查是否需要在 `SKILL.md` 增加或收紧一条规则。

Some files were not shown because too many files have changed in this diff Show More