Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b476176f3b | |||
| cc9af04a40 | |||
| d791f5584c | |||
| ca3156ed93 | |||
| bdfd2ff342 | |||
| ca090cd7f2 | |||
| 2783946cec | |||
| a47e899702 | |||
| 2ba027098f | |||
| aa7a265e76 | |||
| c248c3f0e3 | |||
| 9b3e16cb63 | |||
| 06865939e1 | |||
| 852dee3a69 | |||
| 83deb886b5 | |||
| 17643bc17b | |||
| 4ce08ec89e | |||
| b28acc66d8 | |||
| c2d6a5a0d7 | |||
| ef22c6829e | |||
| 2be0964d73 | |||
| 681aa9e237 | |||
| 10d8800f07 | |||
| e9d2b5fde6 | |||
| f0b71b5bec | |||
| 4423e7df1a | |||
| 12e00bd594 | |||
| f3cd56b78e | |||
| 03d6298145 |
+2
-1
@@ -1,4 +1,4 @@
|
|||||||
# Python(skiff CLI 运行时产物)
|
# Python(pouch CLI 运行时产物)
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
*$py.class
|
*$py.class
|
||||||
@@ -31,3 +31,4 @@ Thumbs.db
|
|||||||
# 本地环境配置(勿提交密钥)
|
# 本地环境配置(勿提交密钥)
|
||||||
.env
|
.env
|
||||||
.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 写入这里。
|
# Developer/Test 只能在任务证据中提出 candidate;只有 Coordinator 写入这里。
|
||||||
version: 1
|
version: 1
|
||||||
updatedAt: "2026-08-01T12:43:06+08:00"
|
updatedAt: "2026-08-01T12:43:06+08:00"
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Agent Skills 仓库 ACK 协作协议(项目覆盖层)
|
# Agent Skills 仓库 ACK 协作协议(项目覆盖层)
|
||||||
|
|
||||||
> 本项目基于 ACK Skill v0.18.0。通用规范由 `/ack` 从 Skill 自身的
|
> 本项目基于 ACK Skill v0.19.0。通用规范由 `/ack` 从 Skill 自身的
|
||||||
> `references/` 读取;本文件只保存当前仓库差异。
|
> `references/` 读取;本文件只保存当前仓库差异。
|
||||||
|
|
||||||
## 项目概览
|
## 项目概览
|
||||||
@@ -9,9 +9,9 @@
|
|||||||
- 技术栈:Python 3、Markdown、JSON Schema
|
- 技术栈:Python 3、Markdown、JSON Schema
|
||||||
- 运行命令:`python3 -m unittest discover -s tests -p 'test_*.py'`
|
- 运行命令:`python3 -m unittest discover -s tests -p 'test_*.py'`
|
||||||
- Base URL:`n/a`
|
- Base URL:`n/a`
|
||||||
- 任务板:`docs/ack/tasks.yaml`
|
- 任务板:`.pouch/ack/tasks.yaml`
|
||||||
- 项目知识:`docs/ack/knowledge.yaml`
|
- 项目知识:`.pouch/ack/knowledge.yaml`
|
||||||
- 覆盖层文件:`docs/ack/project.md`
|
- 覆盖层文件:`.pouch/ack/project.md`
|
||||||
|
|
||||||
## 通用规范(由 ACK Skill 按需读取)
|
## 通用规范(由 ACK Skill 按需读取)
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
## Worker 路由
|
## Worker 路由
|
||||||
|
|
||||||
机器可校验的 worker profile、允许 worktree 和 receipt 只以
|
机器可校验的 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 配置目录中完成黑盒演练。
|
必须使用 fresh worker,并在独立临时项目、独立假 CLI 配置目录中完成黑盒演练。
|
||||||
|
|
||||||
## 路径权限
|
## 路径权限
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
| 路径 | Coordinator | Test | Developer | 说明 |
|
| 路径 | Coordinator | Test | Developer | 说明 |
|
||||||
|------|:-----------:|:----:|:---------:|------|
|
|------|:-----------:|:----:|:---------:|------|
|
||||||
| `docs/ack-feishu-bug-intake.md` | R/W | Read-only | Read-only | 本需求规格与验收契约 |
|
| `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 实现、模板与通用规范 |
|
| `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_unit.py` | Read-only | Read-only | R/W | Developer 白盒测试 |
|
||||||
| `tests/test_ack_feishu_intake_e2e.py` | Read-only | R/W | Read-only | Test 独立黑盒演练与回归测试 |
|
| `tests/test_ack_feishu_intake_e2e.py` | Read-only | R/W | Read-only | Test 独立黑盒演练与回归测试 |
|
||||||
@@ -57,8 +57,8 @@ Test 黑盒复测:
|
|||||||
```bash
|
```bash
|
||||||
python3 -m unittest discover -s tests -p 'test_ack_feishu_intake_e2e.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 -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
|
||||||
python3 skills/ack/scripts/validate_knowledge.py docs/ack/knowledge.yaml --tasks docs/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
|
version: 1
|
||||||
updatedAt: "2026-08-23T20:30:26+08:00"
|
updatedAt: "2026-08-23T20:30:26+08:00"
|
||||||
source: "Coordinator (PM) Agent"
|
source: "Coordinator (PM) Agent"
|
||||||
ackVersion: "0.18.0"
|
ackVersion: "0.19.0"
|
||||||
project:
|
project:
|
||||||
name: "skills"
|
name: "skills"
|
||||||
repoPath: "/home/ace/orca/workspaces/.skills/record-bug"
|
repoPath: "/home/ace/orca/workspaces/.skills/record-bug"
|
||||||
baseUrl: "n/a"
|
baseUrl: "n/a"
|
||||||
devWorktree: "/home/ace/orca/workspaces/.skills/record-bug"
|
devWorktree: "/home/ace/orca/workspaces/.skills/record-bug"
|
||||||
overlayFile: "docs/ack/project.md"
|
overlayFile: ".pouch/ack/project.md"
|
||||||
knowledgeFile: "docs/ack/knowledge.yaml"
|
knowledgeFile: ".pouch/ack/knowledge.yaml"
|
||||||
orchestration:
|
orchestration:
|
||||||
profileVersion: 1
|
profileVersion: 1
|
||||||
mode: "orca"
|
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_unit.py'"
|
||||||
- "python3 -m unittest discover -s tests -p 'test_ack_feishu_intake_e2e.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 -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:
|
browser:
|
||||||
page: "n/a"
|
page: "n/a"
|
||||||
checks:
|
checks:
|
||||||
@@ -507,7 +507,7 @@ tasks:
|
|||||||
explicit tenant-b selection, trusted executable resolution,
|
explicit tenant-b selection, trusted executable resolution,
|
||||||
screenshot containment, stable opaque sourceRef deduplication and
|
screenshot containment, stable opaque sourceRef deduplication and
|
||||||
fail-closed handling for malformed, ambiguous, non-progressing 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.
|
checks also passed.
|
||||||
|
|
||||||
resolution:
|
resolution:
|
||||||
@@ -617,7 +617,7 @@ tasks:
|
|||||||
verification:
|
verification:
|
||||||
commands:
|
commands:
|
||||||
- "python3 -m unittest tests.test_ack_omp_worker tests.test_ack_worker_profiles tests.test_ack_launch_worker"
|
- "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:
|
browser:
|
||||||
page: "n/a"
|
page: "n/a"
|
||||||
checks:
|
checks:
|
||||||
@@ -809,8 +809,8 @@ tasks:
|
|||||||
verification:
|
verification:
|
||||||
commands:
|
commands:
|
||||||
- "python3 -m unittest discover -s tests -p 'test_ack_omp_e2e.py'"
|
- "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_tasks.py .pouch/ack/tasks.yaml"
|
||||||
- "python3 skills/ack/scripts/validate_knowledge.py docs/ack/knowledge.yaml --tasks docs/ack/tasks.yaml"
|
- "python3 skills/ack/scripts/validate_knowledge.py .pouch/ack/knowledge.yaml --tasks .pouch/ack/tasks.yaml"
|
||||||
browser:
|
browser:
|
||||||
page: "n/a"
|
page: "n/a"
|
||||||
checks:
|
checks:
|
||||||
@@ -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 |
|
| **pouch**(本仓库) | [https://git.yumee.top/laily/pouch](https://git.yumee.top/laily/pouch) | skill、规范包、pouch CLI 的当前 SSOT |
|
||||||
| **skiff** | [https://git.yumee.top/laily/skiff](https://git.yumee.top/laily/skiff) | skiff CLI 的独立来源 / 上游同步参考 |
|
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
|
|
||||||
|
需要 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
|
```bash
|
||||||
# 1. 克隆并关联
|
git clone https://git.yumee.top/laily/pouch.git ~/.pouch
|
||||||
git clone https://git.yumee.top/laily/skills.git ~/.skills
|
cd ~/.pouch && ./install.sh
|
||||||
cd ~/.skills && ./install.sh
|
|
||||||
|
|
||||||
# install.sh 会自动把 skiff 项目 skill 安装到所有 Agent
|
# install.sh 只把 pouch skill 装到所有 Agent;其它 skill 用 select / add
|
||||||
# 2. 安装其他 skill
|
pouch select -g
|
||||||
skiff add declarative-openspec-loop -g
|
pouch list
|
||||||
|
pouch status
|
||||||
# 3. 查看状态
|
|
||||||
skiff list
|
|
||||||
skiff status
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -32,22 +29,14 @@ skiff status
|
|||||||
## 仓库结构
|
## 仓库结构
|
||||||
|
|
||||||
```
|
```
|
||||||
skills/
|
skills/ # 自研 skill(SSOT):每个子目录含 SKILL.md(Agent)和 README.md(人类)
|
||||||
├── _template/ # 新建 skill 的脚手架
|
pouch/ # CLI 源码(Python 3)
|
||||||
├── ack/ # 完整 ACK skill,含 references/templates/scripts
|
bin/pouch # CLI 入口
|
||||||
├── declarative-openspec-loop/ # 自研 skill,必须含 SKILL.md
|
catalog.yaml # pouch 预置 Skill 来源目录
|
||||||
│ ├── SKILL.md
|
AGENTS.md # 本文档
|
||||||
│ └── reference.md
|
|
||||||
├── discussion-notes/ # 讨论沉淀笔记
|
|
||||||
│ ├── SKILL.md
|
|
||||||
│ └── reference.md
|
|
||||||
skiff/ # CLI 源码(Python 3)
|
|
||||||
bin/skiff # CLI 入口
|
|
||||||
catalog.yaml # skiff 预置 Skill 来源目录
|
|
||||||
AGENTS.md # 本文档
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**本仓库包含**:`skills/`、`skiff/`、`bin/skiff`、`catalog.yaml`、`AGENTS.md`
|
**本仓库包含**:`skills/`、`pouch/`、`bin/pouch`、`catalog.yaml`、`AGENTS.md`
|
||||||
**本仓库不包含**:各项目的 skill 启用清单
|
**本仓库不包含**:各项目的 skill 启用清单
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -57,28 +46,43 @@ AGENTS.md # 本文档
|
|||||||
### 内置(Builtin)
|
### 内置(Builtin)
|
||||||
|
|
||||||
|
|
||||||
| Skill | 说明 |
|
| Skill | 说明 |
|
||||||
| ---------------------------------------------------------------------- | ------------------------------------------------- |
|
| --- | --- |
|
||||||
| [orc](skills/orc/SKILL.md) | ORC 入口:显式编排开发、版本发布与产物任务,支持 Agent 分档 |
|
| [ack](skills/ack/README.md) | 显式初始化、检查并运行三角色闭环:需求 / bug、测试环境、发版与回归 |
|
||||||
| [ack](skills/ack/SKILL.md) | ACK 入口:显式初始化、检查并运行三角色闭环、测试环境、发版与回归 |
|
| [builder](skills/builder/README.md) | 按统一契约构建和发布 DEB / Docker 镜像 |
|
||||||
| [skiff](skills/skiff/SKILL.md) | 本项目工作流:创建、使用、反馈与更新 builtin skill |
|
| [declarative-openspec-loop](skills/declarative-openspec-loop/README.md) | 你给校验方式,Agent 自动 propose / apply / 校验直到通过 |
|
||||||
| [declarative-openspec-loop](skills/declarative-openspec-loop/SKILL.md) | 声明式编程循环:用户提供校验方式,Agent 自动 propose/apply/校验并迭代直到通过 |
|
| [deployer](skills/deployer/README.md) | 用 Git 里的 Compose 或 Argo CD 清单部署服务 |
|
||||||
| [discussion-notes](skills/discussion-notes/SKILL.md) | 讨论沉淀:边讨论边维护 Markdown 笔记,无 .raw.md |
|
| [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 需要的稳定规范、模板、示例和脚本直接放在自己的目录中,例如
|
Skill 需要的稳定规范、模板、示例和脚本直接放在自己的目录中,例如
|
||||||
`skills/ack/references/`、`templates/`、`examples/` 和 `scripts/`。项目初始化只生成
|
`skills/ack/references/`、`templates/`、`examples/` 和 `scripts/`。项目初始化只生成
|
||||||
项目状态,不复制或链接 Skill 内容:
|
项目状态,不复制或链接 Skill 内容:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
skiff init ack
|
pouch init ack
|
||||||
```
|
```
|
||||||
|
|
||||||
|
builder 与 deployer 的项目接入走各自 skill 的「初始化」模式(检查配置并引导补齐),
|
||||||
|
不要 `pouch init builder` / `pouch init deployer`。
|
||||||
|
|
||||||
### 预置目录(Catalog)
|
### 预置目录(Catalog)
|
||||||
|
|
||||||
在 `catalog.yaml` 中预置,通过 skiff 拉取安装:
|
在 `catalog.yaml` 中预置,通过 pouch 拉取安装:
|
||||||
|
|
||||||
|
|
||||||
| Source | 来源 |
|
| Source | 来源 |
|
||||||
@@ -87,8 +91,8 @@ skiff init ack
|
|||||||
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
skiff fetch waza
|
pouch fetch waza
|
||||||
skiff add waza/think -g
|
pouch add waza/think -g
|
||||||
```
|
```
|
||||||
|
|
||||||
### 自定义仓库(Custom Sources)
|
### 自定义仓库(Custom Sources)
|
||||||
@@ -96,21 +100,21 @@ skiff add waza/think -g
|
|||||||
公司或团队维护、且一个仓库中包含多个 skill 时,使用命名 custom source:
|
公司或团队维护、且一个仓库中包含多个 skill 时,使用命名 custom source:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
skiff source add company \
|
pouch source add company \
|
||||||
git@git.company.com:platform/agent-skills.git \
|
git@git.company.com:platform/agent-skills.git \
|
||||||
--skills-path skills
|
--skills-path skills
|
||||||
skiff list --source company
|
pouch list --source company
|
||||||
skiff add company/internal-review -g
|
pouch add company/internal-review -g
|
||||||
```
|
```
|
||||||
|
|
||||||
也可以接入已有本地 checkout:
|
也可以接入已有本地 checkout:
|
||||||
|
|
||||||
```bash
|
```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 到
|
配置保存在 `~/.config/pouch/config.yaml`,Git source 默认 clone 到
|
||||||
`~/.local/share/skiff/sources/<source>/`。项目 `.skills.yaml` 只记录逻辑
|
`~/.local/share/pouch/sources/<source>/`。项目 `.pouch.yaml` 只记录逻辑
|
||||||
source 名称,每台机器独立配置实际仓库地址。
|
source 名称,每台机器独立配置实际仓库地址。
|
||||||
|
|
||||||
### 社区(External NPM / GitHub)
|
### 社区(External NPM / GitHub)
|
||||||
@@ -127,28 +131,28 @@ npx skills find typescript
|
|||||||
## 设计原则
|
## 设计原则
|
||||||
|
|
||||||
1. **SSOT** — 自研 skill 只存在于 `skills/<name>/`,不在 Agent 目录直接创建
|
1. **SSOT** — 自研 skill 只存在于 `skills/<name>/`,不在 Agent 目录直接创建
|
||||||
2. **项目自治** — 每个项目自己维护 `.skills.yaml`,本仓库不维护项目清单
|
2. **项目自治** — 每个项目自己维护 `.pouch.yaml`,本仓库不维护项目清单
|
||||||
3. **软链优先** — 通过 symlink 映射到 Agent 目录,改 skill 即改 SSOT
|
3. **软链优先** — 通过 symlink 映射到 Agent 目录,改 skill 即改 SSOT
|
||||||
4. **能力内聚** — Skill 使用的规范、模板和脚本与 `SKILL.md` 同目录维护
|
4. **能力内聚** — Skill 使用的规范、模板和脚本与 `SKILL.md` 同目录维护
|
||||||
5. **一体维护** — skill 与 CLI 同仓库维护;需要时再与独立 skiff 仓库同步
|
5. **一体维护** — skill 与 CLI 同仓库维护
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 架构
|
## 架构
|
||||||
|
|
||||||
```
|
```
|
||||||
skills 仓库(本仓库) skiff CLI
|
pouch 仓库(本仓库) pouch CLI
|
||||||
skills/<name>/ ←── skiff install / enable
|
skills/<name>/ ←── pouch install / enable
|
||||||
skiff/ ←── python3 -m skiff
|
pouch/ ←── python3 -m pouch
|
||||||
catalog.yaml ←── skiff add / fetch
|
catalog.yaml ←── pouch add / fetch
|
||||||
↑
|
↑
|
||||||
~/.skills(symlink)
|
~/.pouch(symlink)
|
||||||
│
|
│
|
||||||
┌────┴────┐
|
┌────┴────┐
|
||||||
▼ ▼
|
▼ ▼
|
||||||
~/.cursor/skills/ project/.agents/skills/
|
~/.cursor/skills/ project/.agents/skills/
|
||||||
~/.claude/skills/ project/.claude/skills/
|
~/.claude/skills/ project/.claude/skills/
|
||||||
~/.codex/skills/ project/.skills.yaml
|
~/.codex/skills/ project/.pouch.yaml
|
||||||
~/.agents/skills/ (agents 标准目录,覆盖 OMP)
|
~/.agents/skills/ (agents 标准目录,覆盖 OMP)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -158,16 +162,10 @@ catalog.yaml ←── skiff add / fetch
|
|||||||
| 层级 | 位置 | 维护方式 |
|
| 层级 | 位置 | 维护方式 |
|
||||||
| ---------------- | ---------------------------------- | ------------------------------- |
|
| ---------------- | ---------------------------------- | ------------------------------- |
|
||||||
| **Builtin** | `skills/<name>/` | 本仓库 commit |
|
| **Builtin** | `skills/<name>/` | 本仓库 commit |
|
||||||
| **Catalog** | `catalog.yaml` + checkout 缓存 | `skiff catalog add / fetch` |
|
| **Catalog** | `catalog.yaml` + checkout 缓存 | `pouch catalog add / fetch` |
|
||||||
| **Custom Source** | `~/.local/share/skiff/sources/` 或本地路径 | `skiff source add/fetch` |
|
| **Custom Source** | `~/.local/share/pouch/sources/` 或本地路径 | `pouch source add/fetch` |
|
||||||
| **External NPM** | `node_modules/` | `npx skills add` / `skills-npm` |
|
| **External NPM** | `node_modules/` | `npx skills add` / `skills-npm` |
|
||||||
|
|
||||||
### 非 Skill 资料分类
|
|
||||||
|
|
||||||
| 类型 | 位置 | 维护方式 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| **OpenSpec 配置** | `openspec/` | 本仓库 commit;服务于本仓库自身的规格流程 |
|
|
||||||
|
|
||||||
|
|
||||||
### 多 Agent 路径
|
### 多 Agent 路径
|
||||||
|
|
||||||
@@ -184,10 +182,10 @@ catalog.yaml ←── skiff add / fetch
|
|||||||
|
|
||||||
## 项目级启用
|
## 项目级启用
|
||||||
|
|
||||||
每个项目**自己维护** `.skills.yaml`,不由本仓库管理:
|
每个项目**自己维护** `.pouch.yaml`,不由本仓库管理:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# .skills.yaml(在项目根目录)
|
# .pouch.yaml(在项目根目录)
|
||||||
skills:
|
skills:
|
||||||
- name: declarative-openspec-loop
|
- name: declarative-openspec-loop
|
||||||
source: builtin
|
source: builtin
|
||||||
@@ -205,38 +203,38 @@ targets: # 可选,默认 all
|
|||||||
|
|
||||||
| 概念 | 类比 |
|
| 概念 | 类比 |
|
||||||
| -------------- | --------------------------- |
|
| -------------- | --------------------------- |
|
||||||
| skills 仓库 | npm registry |
|
| pouch 仓库 | npm registry |
|
||||||
| `.skills.yaml` | `package.json` dependencies |
|
| `.pouch.yaml` | `package.json` dependencies |
|
||||||
| `skiff enable` | `npm install` |
|
| `pouch enable` | `npm install` |
|
||||||
| `skiff sync` | `npm ci` |
|
| `pouch sync` | `npm ci` |
|
||||||
|
|
||||||
|
|
||||||
项目级命令:
|
项目级命令:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd ~/code/my-app
|
cd ~/code/my-app
|
||||||
skiff enable declarative-openspec-loop
|
pouch enable declarative-openspec-loop
|
||||||
skiff disable declarative-openspec-loop
|
pouch disable declarative-openspec-loop
|
||||||
skiff sync
|
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 |
|
| `pouch bootstrap` | 将 pouch 项目 skill 全局安装到所有 Agent |
|
||||||
| `skiff list` | 列出所有 skill |
|
| `pouch list` | 列出所有 skill |
|
||||||
| `skiff status` | 安装状态总览 |
|
| `pouch status` | 安装状态总览 |
|
||||||
| `skiff add <name> [-g]` | 项目或全局安装(symlink) |
|
| `pouch add <name> [-g]` | 项目或全局安装(symlink) |
|
||||||
| `skiff remove <name> [-g]` | 移除 symlink |
|
| `pouch remove <name> [-g]` | 移除 symlink |
|
||||||
| `skiff catalog add` / `skiff fetch` | 管理和拉取 catalog source |
|
| `pouch catalog add` / `pouch fetch` | 管理和拉取 catalog source |
|
||||||
|
|
||||||
|
|
||||||
### 草稿与健康检查
|
### 草稿与健康检查
|
||||||
@@ -244,10 +242,10 @@ skiff sync
|
|||||||
|
|
||||||
| 命令 | 说明 |
|
| 命令 | 说明 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `skiff create <name> --idea TEXT [--from-project PATH]` | 从 `_template/` 创建草稿 |
|
| `pouch create <name> --idea TEXT [--from-project PATH]` | 从 `_template/` 创建草稿 |
|
||||||
| `skiff check <name>` | 校验草稿或正式 skill |
|
| `pouch check <name>` | 校验草稿或正式 skill |
|
||||||
| `skiff finalize <name>` | 校验并将草稿转为正式 skill |
|
| `pouch finalize <name>` | 校验并将草稿转为正式 skill |
|
||||||
| `skiff doctor [--fix]` | symlink 健康检查与修复 |
|
| `pouch doctor [--fix]` | symlink 健康检查与修复 |
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -258,7 +256,8 @@ skiff sync
|
|||||||
|
|
||||||
```
|
```
|
||||||
skill-name/
|
skill-name/
|
||||||
├── SKILL.md # 必需
|
├── SKILL.md # 必需,给 Agent
|
||||||
|
├── README.md # 必需,给人类
|
||||||
├── reference.md # 可选
|
├── reference.md # 可选
|
||||||
├── examples.md # 可选
|
├── examples.md # 可选
|
||||||
└── scripts/ # 可选
|
└── scripts/ # 可选
|
||||||
@@ -282,12 +281,12 @@ description: >-
|
|||||||
|
|
||||||
### 新建流程
|
### 新建流程
|
||||||
|
|
||||||
1. `skiff create my-skill --idea "..." --from-project .`
|
1. `pouch create my-skill --idea "..." --from-project .`
|
||||||
2. Agent 编辑 `~/.skills/.drafts/my-skill/SKILL.md`
|
2. Agent 编辑 `~/.pouch/.drafts/my-skill/SKILL.md` 和 `README.md`
|
||||||
3. `skiff check my-skill`
|
3. `pouch check my-skill`
|
||||||
4. 用户确认后执行 `skiff finalize my-skill`
|
4. 用户确认后执行 `pouch finalize my-skill`
|
||||||
5. `skiff add my-skill -a cursor -g -y` 验证
|
5. `pouch add my-skill -a cursor -g -y` 验证
|
||||||
6. 在本仓库 commit;需要的项目再用 `skiff add my-skill` 启用
|
6. 在本仓库 commit;需要的项目再用 `pouch add my-skill` 启用
|
||||||
|
|
||||||
**禁止**在 `~/.cursor/skills/` 或项目 Agent 目录直接创建非 symlink 的 skill。
|
**禁止**在 `~/.cursor/skills/` 或项目 Agent 目录直接创建非 symlink 的 skill。
|
||||||
|
|
||||||
@@ -299,7 +298,7 @@ symlink 正确时,Agent 在项目里改 skill 文件 = 直接改 SSOT:
|
|||||||
|
|
||||||
```
|
```
|
||||||
project/.agents/skills/foo/SKILL.md
|
project/.agents/skills/foo/SKILL.md
|
||||||
→ ~/.skills/skills/foo/SKILL.md
|
→ ~/.pouch/skills/foo/SKILL.md
|
||||||
→ 在本仓库 commit
|
→ 在本仓库 commit
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -314,7 +313,7 @@ Claude Code 对 symlink 支持不稳定:可能无法发现 skill,或写入
|
|||||||
| -------------- | ----------------------------------------------------- |
|
| -------------- | ----------------------------------------------------- |
|
||||||
| Cursor / Codex | symlink,正常 |
|
| Cursor / Codex | symlink,正常 |
|
||||||
| Claude Code | symlink 单个 skill 目录,不要 symlink 整个 `~/.claude/skills/` |
|
| 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`** |
|
| 社区 skill 安装 | **Vercel `npx skills add`** |
|
||||||
| NPM 包内 skill | **skills-npm** / **skill-indexer** |
|
| NPM 包内 skill | **skills-npm** / **skill-indexer** |
|
||||||
| 搜索发现 | **npx skills find** / [skills.sh](https://skills.sh) |
|
| 搜索发现 | **npx skills find** / [skills.sh](https://skills.sh) |
|
||||||
@@ -337,13 +336,15 @@ Claude Code 对 symlink 支持不稳定:可能无法发现 skill,或写入
|
|||||||
|
|
||||||
| 我要… | 命令 | 在哪 |
|
| 我要… | 命令 | 在哪 |
|
||||||
| ---------- | --------------------------------- | ---- |
|
| ---------- | --------------------------------- | ---- |
|
||||||
| 首次安装 | `git clone <repo> ~/.skills && ~/.skills/install.sh` | 任意 |
|
| 首次安装 | `git clone <repo> ~/.pouch && ~/.pouch/install.sh` | 任意 |
|
||||||
| 新建 skill | `skiff create` → Agent 完善 → `check/finalize` | 任意项目 |
|
| 让 Agent 代装 | 说「帮我安装 pouch」或丢仓库 URL | 任意 |
|
||||||
| 全局启用 | `skiff install <name>` | 任意 |
|
| 介绍 / 接入 skill | 说「有哪些 skill」或「把这个项目该用的 skill 接上」 | 项目目录 |
|
||||||
| 项目启用 | `skiff enable <name>` | 项目目录 |
|
| 新建 skill | `pouch create` → Agent 完善 → `check/finalize` | 任意项目 |
|
||||||
| 看状态 | `skiff status` | 任意 |
|
| 全局启用 | `pouch add <name> -g` | 任意 |
|
||||||
|
| 项目启用 | `pouch add <name>` 或 `pouch enable <name>` | 项目目录 |
|
||||||
|
| 看状态 | `pouch status` | 任意 |
|
||||||
| 装社区 skill | `npx skills add owner/repo -g -y` | 任意 |
|
| 装社区 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)
|
- [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)
|
- [Vercel skills CLI](https://github.com/vercel-labs/skills)
|
||||||
- [skills.sh](https://skills.sh)
|
- [skills.sh](https://skills.sh)
|
||||||
- [Cursor Skills 文档](https://cursor.com/docs/context/skills)
|
- [Cursor Skills 文档](https://cursor.com/docs/context/skills)
|
||||||
|
|||||||
@@ -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
|
```bash
|
||||||
git clone https://git.yumee.top/laily/skills.git ~/.skills
|
git clone https://git.yumee.top/laily/pouch.git ~/.pouch
|
||||||
cd ~/.skills
|
cd ~/.pouch
|
||||||
./install.sh # 安装 CLI,并将 skiff 项目 skill 安装到所有 Agent
|
./install.sh
|
||||||
|
|
||||||
skiff add declarative-openspec-loop -g
|
# install.sh 会安装 pouch 命令,并把 pouch 这一个 skill 装到所有 Agent
|
||||||
skiff select # 交互式选择并批量安装
|
# 若当前 shell 找不到 pouch:重开终端,或 source 对应 rc
|
||||||
skiff list
|
|
||||||
skiff status
|
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
|
skills/ # 自研 skill(SSOT):每个子目录含 SKILL.md 和 README.md
|
||||||
skiff/ # CLI 源码(Python 3)
|
pouch/ # CLI 源码(Python 3)
|
||||||
bin/skiff # CLI 入口
|
bin/pouch # CLI 入口
|
||||||
catalog.yaml # skiff 预置 Skill 来源目录
|
catalog.yaml # pouch 预置 Skill 来源目录
|
||||||
AGENTS.md # 详细规范与架构说明
|
AGENTS.md # 详细规范与架构说明
|
||||||
```
|
```
|
||||||
|
|
||||||
| 路径 | 说明 |
|
|
||||||
|------|------|
|
| 路径 | 说明 |
|
||||||
| [skills/](skills/) | 自研 skill,每个子目录含 `SKILL.md`,可附带 references、templates 和 scripts |
|
| ---------------------------- | ----------------------------------------------------- |
|
||||||
| [skiff/](skiff/README.md) | 安装、软链、健康检查 CLI |
|
| [skills/](skills/) | 自研 skill,每个子目录含给人看的 `README.md` 和给 Agent 的 `SKILL.md` |
|
||||||
| [catalog.yaml](catalog.yaml) | skiff 预置 Skill 来源目录 |
|
| [pouch/](pouch/README.md) | 安装、软链、健康检查 CLI |
|
||||||
| [AGENTS.md](AGENTS.md) | 设计原则、编写规范、架构详解 |
|
| [catalog.yaml](catalog.yaml) | pouch 预置 Skill 来源目录 |
|
||||||
|
| [AGENTS.md](AGENTS.md) | 设计原则、编写规范、架构详解 |
|
||||||
|
|
||||||
|
|
||||||
## 自研 Skill
|
## 自研 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
|
```bash
|
||||||
skiff init ack
|
pouch init ack
|
||||||
skiff init ack --project ~/app
|
pouch init ack --project ~/app
|
||||||
```
|
```
|
||||||
|
|
||||||
初始化会生成默认关闭的 `docs/ack/delivery.yaml` 和空的 `docs/ack/regression.yaml`。
|
初始化会生成默认关闭的 `.pouch/ack/delivery.yaml` 和空的 `.pouch/ack/regression.yaml`。
|
||||||
项目可用自然语言让 `/ack` 把测试环境绑到 deployer、维护发版 profile,并在任务
|
项目可用自然语言让 `/ack` 把测试环境绑到 deployer、维护发版 profile,并在任务
|
||||||
验证通过后收获回归用例。
|
验证通过后收获回归用例。
|
||||||
|
|
||||||
|
builder 与 deployer 的项目接入走各自 skill 的「初始化」模式,不要 `pouch init builder` 或 `pouch init deployer`。
|
||||||
|
|
||||||
新建 skill:
|
新建 skill:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
skiff create my-skill --idea "描述要解决的重复问题" --from-project .
|
pouch create my-skill --idea "描述要解决的重复问题" --from-project .
|
||||||
# 由 Agent 完善 ~/.skills/.drafts/my-skill/SKILL.md
|
# 由 Agent 完善 ~/.pouch/.drafts/my-skill/SKILL.md 和 README.md
|
||||||
skiff check my-skill
|
pouch check my-skill
|
||||||
skiff finalize my-skill
|
pouch finalize my-skill
|
||||||
skiff add my-skill -g # 全局安装验证
|
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
|
```bash
|
||||||
skiff add <name> -g # 安装到 ~/.cursor/skills/ 等
|
pouch add <name> -g # 安装到 ~/.cursor/skills/ 等
|
||||||
skiff add <name> -g -a cursor
|
pouch add <name> -g -a cursor
|
||||||
```
|
```
|
||||||
|
|
||||||
### 项目级
|
### 项目级
|
||||||
|
|
||||||
在项目根目录维护 `.skills.yaml`:
|
在项目根目录维护 `.pouch.yaml`:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
skills:
|
skills:
|
||||||
- name: declarative-openspec-loop
|
- name: ack
|
||||||
source: builtin
|
source: builtin
|
||||||
- name: think
|
- name: think
|
||||||
source: catalog:waza
|
source: catalog:waza
|
||||||
@@ -92,9 +152,9 @@ targets: # 可选,默认 all
|
|||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
skiff add declarative-openspec-loop
|
pouch add ack
|
||||||
skiff sync
|
pouch sync
|
||||||
skiff remove declarative-openspec-loop
|
pouch remove ack
|
||||||
```
|
```
|
||||||
|
|
||||||
## Catalog 与 Custom Source
|
## Catalog 与 Custom Source
|
||||||
@@ -102,15 +162,15 @@ skiff remove declarative-openspec-loop
|
|||||||
安装 catalog 中预置的来源:
|
安装 catalog 中预置的来源:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
skiff fetch waza
|
pouch fetch waza
|
||||||
skiff add waza/think -g
|
pouch add waza/think -g
|
||||||
```
|
```
|
||||||
|
|
||||||
接入团队自己的本地目录或 Git 仓库:
|
接入团队自己的本地目录或 Git 仓库:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
skiff source add company --local ~/code/company-skills --skills-path skills
|
pouch source add company --local ~/code/company-skills --skills-path skills
|
||||||
skiff add company/internal-review -g
|
pouch add company/internal-review -g
|
||||||
```
|
```
|
||||||
|
|
||||||
**社区来源**(Vercel CLI):
|
**社区来源**(Vercel CLI):
|
||||||
@@ -124,32 +184,34 @@ npx skills find typescript
|
|||||||
|
|
||||||
```
|
```
|
||||||
本仓库
|
本仓库
|
||||||
├── skills/<name>/ ←── skiff install / enable
|
├── skills/<name>/ ←── pouch add / select
|
||||||
├── catalog.yaml ←── 预置来源发现与 fetch
|
├── catalog.yaml ←── 预置来源发现与 fetch
|
||||||
└── skiff/ ←── python3 -m skiff
|
└── pouch/ ←── python3 -m pouch
|
||||||
↑
|
↑
|
||||||
~/.skills(symlink)
|
~/.pouch(symlink)
|
||||||
│
|
│
|
||||||
┌────┴────────────────┐
|
┌────┴────────────────┐
|
||||||
▼ ▼
|
▼ ▼
|
||||||
~/.cursor/skills/ project/.agents/skills/
|
~/.cursor/skills/ project/.agents/skills/
|
||||||
~/.claude/skills/ project/.claude/skills/
|
~/.claude/skills/ project/.claude/skills/
|
||||||
~/.codex/skills/ project/.skills.yaml
|
~/.codex/skills/ project/.pouch.yaml
|
||||||
~/.agents/skills/ (agents 标准目录,覆盖 OMP)
|
~/.agents/skills/ (agents 标准目录,覆盖 OMP)
|
||||||
```
|
```
|
||||||
|
|
||||||
## 设计原则
|
## 设计原则
|
||||||
|
|
||||||
1. **SSOT** — 自研 skill 只存在于 `skills/<name>/`
|
1. **SSOT** — 自研 skill 只存在于 `skills/<name>/`
|
||||||
2. **项目自治** — 各项目自行维护 `.skills.yaml`
|
2. **项目自治** — 各项目自行维护 `.pouch.yaml`
|
||||||
3. **软链优先** — 通过 symlink 映射到 Agent 目录,改 skill 即改 SSOT
|
3. **软链优先** — 通过 symlink 映射到 Agent 目录,改 skill 即改 SSOT
|
||||||
4. **能力内聚** — Skill 所需规范、模板和脚本与 `SKILL.md` 放在同一目录
|
4. **能力内聚** — Skill 所需规范、模板和脚本与 `SKILL.md` 放在同一目录
|
||||||
5. **一体维护** — skill 与 CLI 同仓库,Python 3 直接运行,无需编译
|
5. **一体维护** — skill 与 CLI 同仓库,Python 3 直接运行,无需编译
|
||||||
|
|
||||||
## 文档
|
## 文档
|
||||||
|
|
||||||
|
- [docs/getting-started.md](docs/getting-started.md):给同事的安装、打版本、构建教程
|
||||||
- [AGENTS.md](AGENTS.md) — 完整规范、多 Agent 路径、编写约定
|
- [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)
|
- [Vercel skills CLI](https://github.com/vercel-labs/skills)
|
||||||
- [skills.sh](https://skills.sh)
|
- [skills.sh](https://skills.sh)
|
||||||
- [Cursor Skills 文档](https://cursor.com/docs/context/skills)
|
- [Cursor Skills 文档](https://cursor.com/docs/context/skills)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# 跟随软链,确保从 ~/.local/bin/skiff 调用时仍能找到仓库根目录
|
# 跟随软链,确保从 ~/.local/bin/pouch 调用时仍能找到仓库根目录
|
||||||
SCRIPT="${BASH_SOURCE[0]}"
|
SCRIPT="${BASH_SOURCE[0]}"
|
||||||
while [ -L "$SCRIPT" ]; do
|
while [ -L "$SCRIPT" ]; do
|
||||||
link_dir="$(cd "$(dirname "$SCRIPT")" && pwd)"
|
link_dir="$(cd "$(dirname "$SCRIPT")" && pwd)"
|
||||||
@@ -11,4 +11,4 @@ done
|
|||||||
REPO_ROOT="$(cd "$(dirname "$SCRIPT")/.." && pwd)"
|
REPO_ROOT="$(cd "$(dirname "$SCRIPT")/.." && pwd)"
|
||||||
|
|
||||||
export PYTHONPATH="${REPO_ROOT}${PYTHONPATH:+:$PYTHONPATH}"
|
export PYTHONPATH="${REPO_ROOT}${PYTHONPATH:+:$PYTHONPATH}"
|
||||||
exec python3 -m skiff "$@"
|
exec python3 -m pouch "$@"
|
||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
# repo: <git-url>
|
# repo: <git-url>
|
||||||
# ref: <branch|tag> (default: main)
|
# ref: <branch|tag> (default: main)
|
||||||
# path: <subpath> (default: .)
|
# path: <subpath> (default: .)
|
||||||
# description: <text> (optional, shown by `skiff select`)
|
# description: <text> (optional, shown by `pouch select`)
|
||||||
# tags: (optional)
|
# tags: (optional)
|
||||||
# - <tag>
|
# - <tag>
|
||||||
#
|
#
|
||||||
|
|||||||
+11
-11
@@ -17,7 +17,7 @@ updated: "2026-07-31T23:31:48+08:00"
|
|||||||
## 背景
|
## 背景
|
||||||
|
|
||||||
本轮评审覆盖 `skills/ack/` 的入口、角色规范、闭环流程、项目模板、任务板
|
本轮评审覆盖 `skills/ack/` 的入口、角色规范、闭环流程、项目模板、任务板
|
||||||
schema、校验脚本、Orca 适配器,以及 `skiff init ack` 的真实运行路径。
|
schema、校验脚本、Orca 适配器,以及 `pouch init ack` 的真实运行路径。
|
||||||
|
|
||||||
评审主要回答两个问题:
|
评审主要回答两个问题:
|
||||||
|
|
||||||
@@ -216,15 +216,15 @@ Deferred,当前 launcher 必须 fail closed。
|
|||||||
|
|
||||||
## P0:初始化需要原子化
|
## P0:初始化需要原子化
|
||||||
|
|
||||||
`skiff init ack` 会先写 `project.md` 和 `tasks.yaml`,再运行任务板校验。缺少 PyYAML
|
`pouch init ack` 会先写 `project.md` 和 `tasks.yaml`,再运行任务板校验。缺少 PyYAML
|
||||||
时,命令会失败但保留两个文件。再次运行又会因为拒绝覆盖而失败。
|
时,命令会失败但保留两个文件。再次运行又会因为拒绝覆盖而失败。
|
||||||
|
|
||||||
相关位置:
|
相关位置:
|
||||||
|
|
||||||
- `requirements.txt:1`
|
- `requirements.txt:1`
|
||||||
- `skills/ack/scripts/validate_tasks.py:37`
|
- `skills/ack/scripts/validate_tasks.py:37`
|
||||||
- `skiff/cli.py:1144`
|
- `pouch/cli.py:1144`
|
||||||
- `skiff/cli.py:1156`
|
- `pouch/cli.py:1156`
|
||||||
|
|
||||||
本轮在不含第三方包的隔离 Python 环境中复现了这个状态。
|
本轮在不含第三方包的隔离 Python 环境中复现了这个状态。
|
||||||
|
|
||||||
@@ -232,7 +232,7 @@ Deferred,当前 launcher 必须 fail closed。
|
|||||||
|
|
||||||
- 在临时目录渲染和校验,全部通过后再原子 rename。
|
- 在临时目录渲染和校验,全部通过后再原子 rename。
|
||||||
- 失败时只清理由本次调用创建的临时文件。
|
- 失败时只清理由本次调用创建的临时文件。
|
||||||
- 提供 `skiff init ack --repair` 或等价恢复路径。
|
- 提供 `pouch init ack --repair` 或等价恢复路径。
|
||||||
- 默认模板使用 `tasks: []`,完整示例继续放在 `examples/`。
|
- 默认模板使用 `tasks: []`,完整示例继续放在 `examples/`。
|
||||||
- CLI 输出“脚手架已创建,待配置”,检查通过后再称为“初始化完成”。
|
- CLI 输出“脚手架已创建,待配置”,检查通过后再称为“初始化完成”。
|
||||||
|
|
||||||
@@ -332,7 +332,7 @@ ACK 默认不提交、不推送。隔离 worktree 中的任务即使复测通过
|
|||||||
2. 把 Orca adapter 改成每轮 Developer/Test 双任务。
|
2. 把 Orca adapter 改成每轮 Developer/Test 双任务。
|
||||||
3. 定义同 worktree 默认策略和跨 worktree transfer。
|
3. 定义同 worktree 默认策略和跨 worktree transfer。
|
||||||
4. 改造 worker launcher 与安全授权。
|
4. 改造 worker launcher 与安全授权。
|
||||||
5. 原子化 `skiff init ack`,空任务板作为默认模板。
|
5. 原子化 `pouch init ack`,空任务板作为默认模板。
|
||||||
6. 引入追加式 attempts、幂等恢复和失败分类。
|
6. 引入追加式 attempts、幂等恢复和失败分类。
|
||||||
7. 统一 schema 与语义校验,补齐对抗性 fixture。
|
7. 统一 schema 与语义校验,补齐对抗性 fixture。
|
||||||
8. 区分工作空间验证、集成验证和发布状态。
|
8. 区分工作空间验证、集成验证和发布状态。
|
||||||
@@ -342,7 +342,7 @@ ACK 默认不提交、不推送。隔离 worktree 中的任务即使复测通过
|
|||||||
|
|
||||||
本轮执行了:
|
本轮执行了:
|
||||||
|
|
||||||
- `skiff check ack`:通过。这个命令只证明 Skill 的元数据和基础结构有效。
|
- `pouch check ack`:通过。这个命令只证明 Skill 的元数据和基础结构有效。
|
||||||
- `validate_tasks.py examples/tasks.example.yaml`:通过,当前机器使用内置规则。
|
- `validate_tasks.py examples/tasks.example.yaml`:通过,当前机器使用内置规则。
|
||||||
- `validate_worker_command.py --self-test`:7 项通过。
|
- `validate_worker_command.py --self-test`:7 项通过。
|
||||||
- `python3 -m unittest discover -s tests -v`:58 项通过。
|
- `python3 -m unittest discover -s tests -v`:58 项通过。
|
||||||
@@ -394,10 +394,10 @@ ACK 需要增加第三类项目事实,用来保存跨任务复用、会改变
|
|||||||
| `tasks.yaml` | 当前任务状态、attempt 和执行证据 | Coordinator |
|
| `tasks.yaml` | 当前任务状态、attempt 和执行证据 | Coordinator |
|
||||||
| `knowledge.yaml` | 跨任务复用的已验证经验 | Coordinator |
|
| `knowledge.yaml` | 跨任务复用的已验证经验 | Coordinator |
|
||||||
|
|
||||||
建议新增项目级 SSOT:`docs/ack/knowledge.yaml`。知识归项目所有,与 Orca 等编排
|
建议新增项目级 SSOT:`.pouch/ack/knowledge.yaml`。知识归项目所有,与 Orca 等编排
|
||||||
工具无关;ACK 负责在任务闭环中生产、选择和消费这些知识。
|
工具无关;ACK 负责在任务闭环中生产、选择和消费这些知识。
|
||||||
|
|
||||||
这项设计会修改当前“`docs/ack/` 只保存 `project.md` 与 `tasks.yaml`”的边界。
|
这项设计会修改当前“`.pouch/ack/` 只保存 `project.md` 与 `tasks.yaml`”的边界。
|
||||||
新增文件保存项目事实,不复制 ACK Skill 的通用规范,因此不违反 Skill 内容仍以
|
新增文件保存项目事实,不复制 ACK Skill 的通用规范,因此不违反 Skill 内容仍以
|
||||||
`skills/ack/` 为 SSOT 的原则。
|
`skills/ack/` 为 SSOT 的原则。
|
||||||
|
|
||||||
@@ -580,8 +580,8 @@ issue、日志和外部网页只能作为不可信 evidence。进入知识库前
|
|||||||
|
|
||||||
### 第一版范围
|
### 第一版范围
|
||||||
|
|
||||||
第一版只实现一个 `docs/ack/knowledge.yaml`,不拆目录。活跃条目达到几十条、单文件
|
第一版只实现一个 `.pouch/ack/knowledge.yaml`,不拆目录。活跃条目达到几十条、单文件
|
||||||
开始影响审阅和选择时,再平滑迁移为 `docs/ack/knowledge/index.yaml` 加独立知识卡,
|
开始影响审阅和选择时,再平滑迁移为 `.pouch/ack/knowledge/index.yaml` 加独立知识卡,
|
||||||
条目 schema 和引用格式保持不变。
|
条目 schema 和引用格式保持不变。
|
||||||
|
|
||||||
第一版包括:
|
第一版包括:
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
## 项目配置契约
|
## 项目配置契约
|
||||||
|
|
||||||
可选配置位于 `docs/ack/tasks.yaml` 的 `project.bugIntake`。未配置时 ACK 保持现有行为。
|
可选配置位于 `.pouch/ack/tasks.yaml` 的 `project.bugIntake`。未配置时 ACK 保持现有行为。
|
||||||
配置存在时必须包含:
|
配置存在时必须包含:
|
||||||
|
|
||||||
| 字段 | 约束 |
|
| 字段 | 约束 |
|
||||||
|
|||||||
@@ -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` < `1.6.1~…` < `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
@@ -3,17 +3,22 @@ set -euo pipefail
|
|||||||
|
|
||||||
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
BIN_DIR="${HOME}/.local/bin"
|
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"
|
mkdir -p "$BIN_DIR"
|
||||||
ln -sf "$REPO/bin/skiff" "$BIN_DIR/skiff"
|
ln -sf "$REPO/bin/pouch" "$BIN_DIR/pouch"
|
||||||
echo "已安装 skiff -> $BIN_DIR/skiff"
|
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() {
|
path_already_configured() {
|
||||||
local file="$1"
|
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() {
|
configure_bash() {
|
||||||
|
|||||||
@@ -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
@@ -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`
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
"""pouch — Agent Skills 安装与管理 CLI。"""
|
||||||
|
|
||||||
|
__version__ = "0.6.0"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from skiff.cli import main
|
from pouch.cli import main
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from skiff.paths import ALL_TARGETS
|
from pouch.paths import ALL_TARGETS
|
||||||
|
|
||||||
AGENT_ALIASES: dict[str, str] = {
|
AGENT_ALIASES: dict[str, str] = {
|
||||||
"cursor": "cursor",
|
"cursor": "cursor",
|
||||||
@@ -6,8 +6,8 @@ import hashlib
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from skiff import yaml_io
|
from pouch import yaml_io
|
||||||
from skiff.paths import CATALOG_FILE, LEGACY_REGISTRY_FILE
|
from pouch.paths import CATALOG_FILE, LEGACY_REGISTRY_FILE
|
||||||
|
|
||||||
|
|
||||||
def load_catalog(path: Path | None = None) -> dict[str, dict[str, Any]]:
|
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:
|
def catalog_repo_path(entry: dict[str, Any]) -> Path:
|
||||||
"""Return the shared checkout path for a repo/ref pair."""
|
"""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", ""))
|
repo = str(entry.get("repo", ""))
|
||||||
ref = str(entry.get("ref", "main"))
|
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:
|
def catalog_checkout_path(name: str, entry: dict[str, Any]) -> Path:
|
||||||
"""Use a local repo directly, otherwise return its external checkout."""
|
"""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", ""))
|
configured_repo = str(entry.get("repo", ""))
|
||||||
local_repo = Path(catalog_repo(entry))
|
local_repo = Path(catalog_repo(entry))
|
||||||
+97
-96
@@ -1,4 +1,4 @@
|
|||||||
"""skiff CLI 入口。"""
|
"""pouch CLI 入口。"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -15,28 +15,29 @@ import tempfile
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from skiff import __version__
|
from pouch import __version__
|
||||||
from skiff.agents import flatten_agent_args, resolve_agent_args
|
from pouch.agents import flatten_agent_args, resolve_agent_args
|
||||||
from skiff.gitops import publish as git_publish
|
from pouch.gitops import publish as git_publish
|
||||||
from skiff.paths import (
|
from pouch.paths import (
|
||||||
ALL_TARGETS,
|
ALL_TARGETS,
|
||||||
DRAFTS_DIR,
|
DRAFTS_DIR,
|
||||||
CATALOG_CACHE_DIR,
|
CATALOG_CACHE_DIR,
|
||||||
CONFIG_FILE,
|
CONFIG_FILE,
|
||||||
SKILLS_DIR,
|
SKILLS_DIR,
|
||||||
SKILLS_HOME,
|
POUCH_HOME,
|
||||||
TEMPLATE_DIR,
|
TEMPLATE_DIR,
|
||||||
agent_skill_dir,
|
agent_skill_dir,
|
||||||
ensure_skills_home,
|
ensure_pouch_home,
|
||||||
|
project_manifest,
|
||||||
)
|
)
|
||||||
from skiff.project import (
|
from pouch.project import (
|
||||||
add_skill_to_manifest,
|
add_skill_to_manifest,
|
||||||
iter_manifest_skills,
|
iter_manifest_skills,
|
||||||
load_manifest,
|
load_manifest,
|
||||||
remove_skill_from_manifest,
|
remove_skill_from_manifest,
|
||||||
resolve_manifest_skill,
|
resolve_manifest_skill,
|
||||||
)
|
)
|
||||||
from skiff.catalog import (
|
from pouch.catalog import (
|
||||||
discover_catalog_skills,
|
discover_catalog_skills,
|
||||||
catalog_checkout_path,
|
catalog_checkout_path,
|
||||||
catalog_skill_path,
|
catalog_skill_path,
|
||||||
@@ -44,8 +45,8 @@ from skiff.catalog import (
|
|||||||
catalog_repo,
|
catalog_repo,
|
||||||
save_catalog,
|
save_catalog,
|
||||||
)
|
)
|
||||||
from skiff.selector import SkillChoice, select_skills
|
from pouch.selector import SkillChoice, select_skills
|
||||||
from skiff.skills import (
|
from pouch.skills import (
|
||||||
list_custom_skills,
|
list_custom_skills,
|
||||||
list_builtin_skills,
|
list_builtin_skills,
|
||||||
builtin_skill_path,
|
builtin_skill_path,
|
||||||
@@ -57,7 +58,7 @@ from skiff.skills import (
|
|||||||
validate_skill_dir,
|
validate_skill_dir,
|
||||||
validate_skill_name,
|
validate_skill_name,
|
||||||
)
|
)
|
||||||
from skiff.sources import (
|
from pouch.sources import (
|
||||||
discover_source_skills,
|
discover_source_skills,
|
||||||
fetch_source,
|
fetch_source,
|
||||||
load_sources,
|
load_sources,
|
||||||
@@ -66,8 +67,8 @@ from skiff.sources import (
|
|||||||
source_skills_root,
|
source_skills_root,
|
||||||
validate_source_name,
|
validate_source_name,
|
||||||
)
|
)
|
||||||
from skiff.yaml_io import safe_dump
|
from pouch.yaml_io import safe_dump
|
||||||
from skiff.symlinks import check_link, copy_template, create_link, find_repo_root, remove_link
|
from pouch.symlinks import check_link, copy_template, create_link, find_repo_root, remove_link
|
||||||
|
|
||||||
_RENAME_NOREPLACE = 1
|
_RENAME_NOREPLACE = 1
|
||||||
|
|
||||||
@@ -485,7 +486,7 @@ def _remove_skill(
|
|||||||
|
|
||||||
|
|
||||||
def cmd_list(args: argparse.Namespace) -> None:
|
def cmd_list(args: argparse.Namespace) -> None:
|
||||||
ensure_skills_home()
|
ensure_pouch_home()
|
||||||
source_filter = normalize_source(args.source)
|
source_filter = normalize_source(args.source)
|
||||||
builtin = list_builtin_skills()
|
builtin = list_builtin_skills()
|
||||||
catalog = load_catalog()
|
catalog = load_catalog()
|
||||||
@@ -528,18 +529,18 @@ def cmd_list(args: argparse.Namespace) -> None:
|
|||||||
|
|
||||||
def cmd_bootstrap(args: argparse.Namespace) -> None:
|
def cmd_bootstrap(args: argparse.Namespace) -> None:
|
||||||
del args
|
del args
|
||||||
ensure_skills_home()
|
ensure_pouch_home()
|
||||||
project_skill = "skiff"
|
project_skill = "pouch"
|
||||||
builtin_skill_path(project_skill)
|
builtin_skill_path(project_skill)
|
||||||
_install_skill(project_skill, list(ALL_TARGETS), project_root=None)
|
_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:
|
def cmd_update(args: argparse.Namespace) -> None:
|
||||||
del args
|
del args
|
||||||
ensure_skills_home()
|
ensure_pouch_home()
|
||||||
_print(f"更新 skiff: {SKILLS_HOME}")
|
_print(f"更新 pouch: {POUCH_HOME}")
|
||||||
subprocess.run(["git", "-C", str(SKILLS_HOME), "pull"], check=True)
|
subprocess.run(["git", "-C", str(POUCH_HOME), "pull"], check=True)
|
||||||
|
|
||||||
|
|
||||||
def _installed_links(
|
def _installed_links(
|
||||||
@@ -558,7 +559,7 @@ def _installed_links(
|
|||||||
|
|
||||||
|
|
||||||
def cmd_status(args: argparse.Namespace) -> None:
|
def cmd_status(args: argparse.Namespace) -> None:
|
||||||
ensure_skills_home()
|
ensure_pouch_home()
|
||||||
targets = resolve_agent_args(flatten_agent_args(args.agents))
|
targets = resolve_agent_args(flatten_agent_args(args.agents))
|
||||||
builtin = list_builtin_skills()
|
builtin = list_builtin_skills()
|
||||||
catalog = load_catalog()
|
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)
|
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")
|
_print(f"agents: {', '.join(targets)}\n")
|
||||||
|
|
||||||
for package in unfetched_catalog:
|
for package in unfetched_catalog:
|
||||||
@@ -604,10 +605,10 @@ def cmd_status(args: argparse.Namespace) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def _print_available_skills() -> None:
|
def _print_available_skills() -> None:
|
||||||
ensure_skills_home()
|
ensure_pouch_home()
|
||||||
builtin = list_builtin_skills()
|
builtin = list_builtin_skills()
|
||||||
if not builtin:
|
if not builtin:
|
||||||
_print("~/.skills/skills/ 中没有自研 skill")
|
_print("~/.pouch/skills/ 中没有自研 skill")
|
||||||
return
|
return
|
||||||
|
|
||||||
_print(f"来源: {SKILLS_DIR}\n")
|
_print(f"来源: {SKILLS_DIR}\n")
|
||||||
@@ -621,14 +622,14 @@ def _print_available_skills() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def cmd_add(args: argparse.Namespace) -> None:
|
def cmd_add(args: argparse.Namespace) -> None:
|
||||||
ensure_skills_home()
|
ensure_pouch_home()
|
||||||
|
|
||||||
if args.list_available:
|
if args.list_available:
|
||||||
if args.source:
|
if args.source:
|
||||||
cmd_list(argparse.Namespace(source=args.source))
|
cmd_list(argparse.Namespace(source=args.source))
|
||||||
else:
|
else:
|
||||||
_print_available_skills()
|
_print_available_skills()
|
||||||
_print("使用 skiff add <name> 安装,或 skiff add <name> -g 全局安装")
|
_print("使用 pouch add <name> 安装,或 pouch add <name> -g 全局安装")
|
||||||
return
|
return
|
||||||
|
|
||||||
if args.all:
|
if args.all:
|
||||||
@@ -647,7 +648,7 @@ def cmd_add(args: argparse.Namespace) -> None:
|
|||||||
else:
|
else:
|
||||||
names = _collect_skill_names(args.skills, args.skills_flag)
|
names = _collect_skill_names(args.skills, args.skills_flag)
|
||||||
if not names:
|
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))
|
targets = resolve_agent_args(flatten_agent_args(args.agents))
|
||||||
|
|
||||||
project_root = None if args.global_scope else _project_root(args.project)
|
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)
|
(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_before = (
|
||||||
manifest_path.read_bytes()
|
manifest_path.read_bytes()
|
||||||
if manifest_path and manifest_path.is_file()
|
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:
|
def cmd_select(args: argparse.Namespace) -> None:
|
||||||
ensure_skills_home()
|
ensure_pouch_home()
|
||||||
if not sys.stdin.isatty() or not sys.stdout.isatty():
|
if not sys.stdin.isatty() or not sys.stdout.isatty():
|
||||||
raise SystemExit(
|
raise SystemExit(
|
||||||
"`skiff select` 需要交互式终端;非交互环境请使用 `skiff add <name>...`"
|
"`pouch select` 需要交互式终端;非交互环境请使用 `pouch add <name>...`"
|
||||||
)
|
)
|
||||||
|
|
||||||
targets = resolve_agent_args(flatten_agent_args(args.agents))
|
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)
|
names = sorted(selected - selected_installed_keys)
|
||||||
|
|
||||||
failures: list[tuple[str, str]] = []
|
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)
|
successful = set(selected & selected_installed_keys)
|
||||||
for name in names:
|
for name in names:
|
||||||
skill_name, source = choice_requests[name]
|
skill_name, source = choice_requests[name]
|
||||||
@@ -915,7 +916,7 @@ def cmd_select(args: argparse.Namespace) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def cmd_remove(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)
|
project_root = None if args.global_scope else _project_root(args.project)
|
||||||
targets = resolve_agent_args(flatten_agent_args(args.agents))
|
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)
|
names = _collect_skill_names(args.skills, args.skills_flag)
|
||||||
|
|
||||||
if not names:
|
if not names:
|
||||||
raise SystemExit("请指定 skill 名称,或使用 skiff remove --all")
|
raise SystemExit("请指定 skill 名称,或使用 pouch remove --all")
|
||||||
|
|
||||||
catalog = load_catalog()
|
catalog = load_catalog()
|
||||||
sources = load_sources()
|
sources = load_sources()
|
||||||
@@ -958,14 +959,14 @@ def cmd_remove(args: argparse.Namespace) -> None:
|
|||||||
for name in dict.fromkeys(expanded):
|
for name in dict.fromkeys(expanded):
|
||||||
total += _remove_skill(name, targets, project_root=project_root)
|
total += _remove_skill(name, targets, project_root=project_root)
|
||||||
if project_root is not None:
|
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:
|
if total == 0:
|
||||||
_print("没有移除任何 skill")
|
_print("没有移除任何 skill")
|
||||||
|
|
||||||
|
|
||||||
def cmd_publish(args: argparse.Namespace) -> None:
|
def cmd_publish(args: argparse.Namespace) -> None:
|
||||||
ensure_skills_home()
|
ensure_pouch_home()
|
||||||
paths = args.paths or ["."]
|
paths = args.paths or ["."]
|
||||||
git_publish(
|
git_publish(
|
||||||
paths=paths,
|
paths=paths,
|
||||||
@@ -976,7 +977,7 @@ def cmd_publish(args: argparse.Namespace) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def cmd_catalog_add(args: argparse.Namespace) -> None:
|
def cmd_catalog_add(args: argparse.Namespace) -> None:
|
||||||
ensure_skills_home()
|
ensure_pouch_home()
|
||||||
validate_skill_name(args.name)
|
validate_skill_name(args.name)
|
||||||
catalog = load_catalog()
|
catalog = load_catalog()
|
||||||
if args.name in 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:
|
def cmd_fetch(args: argparse.Namespace) -> None:
|
||||||
ensure_skills_home()
|
ensure_pouch_home()
|
||||||
catalog = load_catalog()
|
catalog = load_catalog()
|
||||||
if args.name not in catalog:
|
if args.name not in catalog:
|
||||||
raise SystemExit(f"catalog 中不存在: {args.name}")
|
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:
|
def cmd_sync(args: argparse.Namespace) -> None:
|
||||||
ensure_skills_home()
|
ensure_pouch_home()
|
||||||
root = _project_root(args.project)
|
root = _project_root(args.project)
|
||||||
manifest_path = root / ".skills.yaml"
|
manifest_path = project_manifest(root)
|
||||||
if not manifest_path.is_file():
|
if not manifest_path.is_file():
|
||||||
raise SystemExit(f"未找到 {manifest_path}")
|
raise SystemExit(f"未找到 {manifest_path}")
|
||||||
|
|
||||||
@@ -1133,7 +1134,7 @@ def cmd_sync(args: argparse.Namespace) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def cmd_create(args: argparse.Namespace) -> None:
|
def cmd_create(args: argparse.Namespace) -> None:
|
||||||
ensure_skills_home()
|
ensure_pouch_home()
|
||||||
validate_skill_name(args.name)
|
validate_skill_name(args.name)
|
||||||
if not TEMPLATE_DIR.is_dir():
|
if not TEMPLATE_DIR.is_dir():
|
||||||
raise SystemExit(f"模板目录不存在: {TEMPLATE_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")
|
(dst / "brief.yaml").write_text(safe_dump(brief), encoding="utf-8")
|
||||||
_print(f"草稿已创建: {dst}")
|
_print(f"草稿已创建: {dst}")
|
||||||
_print(f"下一步: 请完善 skiff 草稿 {args.name}")
|
_print(f"下一步: 请完善 pouch 草稿 {args.name}")
|
||||||
_print(f"完成后运行: skiff check {args.name} && skiff finalize {args.name}")
|
_print(f"完成后运行: pouch check {args.name} && pouch finalize {args.name}")
|
||||||
|
|
||||||
|
|
||||||
def _draft_or_builtin_path(name: str) -> tuple[Path, str]:
|
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:
|
def cmd_check(args: argparse.Namespace) -> None:
|
||||||
ensure_skills_home()
|
ensure_pouch_home()
|
||||||
validate_skill_name(args.name)
|
validate_skill_name(args.name)
|
||||||
path, kind = _draft_or_builtin_path(args.name)
|
path, kind = _draft_or_builtin_path(args.name)
|
||||||
issues = validate_skill_dir(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:
|
def cmd_finalize(args: argparse.Namespace) -> None:
|
||||||
ensure_skills_home()
|
ensure_pouch_home()
|
||||||
validate_skill_name(args.name)
|
validate_skill_name(args.name)
|
||||||
draft = DRAFTS_DIR / args.name
|
draft = DRAFTS_DIR / args.name
|
||||||
if not draft.is_dir():
|
if not draft.is_dir():
|
||||||
@@ -1221,25 +1222,25 @@ def cmd_finalize(args: argparse.Namespace) -> None:
|
|||||||
if brief.exists():
|
if brief.exists():
|
||||||
brief.unlink()
|
brief.unlink()
|
||||||
_print(f"已完成 skill: {final}")
|
_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:
|
def cmd_doctor(args: argparse.Namespace) -> None:
|
||||||
ensure_skills_home()
|
ensure_pouch_home()
|
||||||
targets = resolve_agent_args(flatten_agent_args(args.agents))
|
targets = resolve_agent_args(flatten_agent_args(args.agents))
|
||||||
issues = 0
|
issues = 0
|
||||||
|
|
||||||
_print(f"skills 仓库: {SKILLS_HOME.resolve()}")
|
_print(f"pouch 仓库: {POUCH_HOME.resolve()}")
|
||||||
if SKILLS_HOME.is_symlink():
|
if POUCH_HOME.is_symlink():
|
||||||
if not SKILLS_HOME.resolve().is_dir():
|
if not POUCH_HOME.resolve().is_dir():
|
||||||
_err(f"✗ ~/.skills 指向无效路径: {SKILLS_HOME.resolve()}")
|
_err(f"✗ ~/.pouch 指向无效路径: {POUCH_HOME.resolve()}")
|
||||||
issues += 1
|
issues += 1
|
||||||
else:
|
else:
|
||||||
_print("✓ ~/.skills 软链正常")
|
_print("✓ ~/.pouch 软链正常")
|
||||||
elif SKILLS_HOME.is_dir() and (SKILLS_HOME / "skills").is_dir():
|
elif POUCH_HOME.is_dir() and (POUCH_HOME / "skills").is_dir():
|
||||||
_print("✓ ~/.skills 为本地仓库目录")
|
_print("✓ ~/.pouch 为本地仓库目录")
|
||||||
else:
|
else:
|
||||||
_err("✗ ~/.skills 未正确配置")
|
_err("✗ ~/.pouch 未正确配置")
|
||||||
issues += 1
|
issues += 1
|
||||||
|
|
||||||
for name in list_builtin_skills():
|
for name in list_builtin_skills():
|
||||||
@@ -1278,13 +1279,13 @@ def cmd_doctor(args: argparse.Namespace) -> None:
|
|||||||
else:
|
else:
|
||||||
_print(f"\n发现 {issues} 个问题")
|
_print(f"\n发现 {issues} 个问题")
|
||||||
if not args.fix:
|
if not args.fix:
|
||||||
_print("提示: 使用 skiff doctor --fix 尝试自动修复软链")
|
_print("提示: 使用 pouch doctor --fix 尝试自动修复软链")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def cmd_init(args: argparse.Namespace) -> None:
|
def cmd_init(args: argparse.Namespace) -> None:
|
||||||
"""使用 builtin skill 自带的模板初始化目标项目状态。"""
|
"""使用 builtin skill 自带的模板初始化目标项目状态。"""
|
||||||
ensure_skills_home()
|
ensure_pouch_home()
|
||||||
validate_skill_name(args.name)
|
validate_skill_name(args.name)
|
||||||
skills_root = SKILLS_DIR.resolve()
|
skills_root = SKILLS_DIR.resolve()
|
||||||
skill_source = (SKILLS_DIR / args.name).resolve()
|
skill_source = (SKILLS_DIR / args.name).resolve()
|
||||||
@@ -1307,7 +1308,7 @@ def cmd_init(args: argparse.Namespace) -> None:
|
|||||||
initial_project_stat.st_ino,
|
initial_project_stat.st_ino,
|
||||||
stat.S_IFMT(initial_project_stat.st_mode),
|
stat.S_IFMT(initial_project_stat.st_mode),
|
||||||
)
|
)
|
||||||
destination = project / "docs" / args.name
|
destination = project / ".pouch" / args.name
|
||||||
project_file = destination / "project.md"
|
project_file = destination / "project.md"
|
||||||
tasks_file = destination / "tasks.yaml"
|
tasks_file = destination / "tasks.yaml"
|
||||||
knowledge_file = destination / "knowledge.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()]
|
missing = [path for path, _ in template_targets if not path.is_file()]
|
||||||
if missing:
|
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}")
|
raise SystemExit(f"skill 缺少初始化模板: {paths}")
|
||||||
validator = skill_source / "scripts" / "validate_tasks.py"
|
validator = skill_source / "scripts" / "validate_tasks.py"
|
||||||
knowledge_validator = skill_source / "scripts" / "validate_knowledge.py"
|
knowledge_validator = skill_source / "scripts" / "validate_knowledge.py"
|
||||||
@@ -1368,13 +1369,13 @@ def cmd_init(args: argparse.Namespace) -> None:
|
|||||||
"<project_name>": project.name,
|
"<project_name>": project.name,
|
||||||
"<repo_path>": str(project),
|
"<repo_path>": str(project),
|
||||||
"<dev_worktree>": 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_version>": ack_version,
|
||||||
"<接入时的 ack skill 版本>": ack_version,
|
"<接入时的 ack skill 版本>": ack_version,
|
||||||
"<YYYY-MM-DDTHH:mm:ss+TZ>": now,
|
"<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)
|
staging = Path(temp_dir)
|
||||||
staged_files: dict[Path, Path] = {}
|
staged_files: dict[Path, Path] = {}
|
||||||
rendered_files: dict[Path, str] = {}
|
rendered_files: dict[Path, str] = {}
|
||||||
@@ -1456,10 +1457,10 @@ def cmd_init(args: argparse.Namespace) -> None:
|
|||||||
raise SystemExit(f"拒绝覆盖已有路径: {paths}")
|
raise SystemExit(f"拒绝覆盖已有路径: {paths}")
|
||||||
|
|
||||||
project_fd: int | None = None
|
project_fd: int | None = None
|
||||||
docs_fd: int | None = None
|
pouch_fd: int | None = None
|
||||||
transaction_fd: int | None = None
|
transaction_fd: int | None = None
|
||||||
staging_fd: int | None = None
|
staging_fd: int | None = None
|
||||||
docs_created = False
|
pouch_created = False
|
||||||
transaction_name: str | None = None
|
transaction_name: str | None = None
|
||||||
staged_names: list[str] = []
|
staged_names: list[str] = []
|
||||||
published = False
|
published = False
|
||||||
@@ -1487,8 +1488,8 @@ def cmd_init(args: argparse.Namespace) -> None:
|
|||||||
project,
|
project,
|
||||||
phase="初始化",
|
phase="初始化",
|
||||||
)
|
)
|
||||||
docs_fd, docs_created = _open_or_create_directory_at(project_fd, "docs")
|
pouch_fd, pouch_created = _open_or_create_directory_at(project_fd, ".pouch")
|
||||||
if docs_created:
|
if pouch_created:
|
||||||
os.fsync(project_fd)
|
os.fsync(project_fd)
|
||||||
_assert_open_directory_path(
|
_assert_open_directory_path(
|
||||||
project_fd,
|
project_fd,
|
||||||
@@ -1496,15 +1497,15 @@ def cmd_init(args: argparse.Namespace) -> None:
|
|||||||
phase="初始化",
|
phase="初始化",
|
||||||
)
|
)
|
||||||
_assert_open_directory_path(
|
_assert_open_directory_path(
|
||||||
docs_fd,
|
pouch_fd,
|
||||||
project / "docs",
|
project / ".pouch",
|
||||||
phase="初始化",
|
phase="初始化",
|
||||||
label="docs 目录",
|
label=".pouch 目录",
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
destination_stat = os.stat(
|
destination_stat = os.stat(
|
||||||
args.name,
|
args.name,
|
||||||
dir_fd=docs_fd,
|
dir_fd=pouch_fd,
|
||||||
follow_symlinks=False,
|
follow_symlinks=False,
|
||||||
)
|
)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
@@ -1513,14 +1514,14 @@ def cmd_init(args: argparse.Namespace) -> None:
|
|||||||
if stat.S_ISLNK(destination_stat.st_mode):
|
if stat.S_ISLNK(destination_stat.st_mode):
|
||||||
raise SystemExit(
|
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):
|
for _ in range(32):
|
||||||
candidate = f".{args.name}-init-{secrets.token_hex(8)}"
|
candidate = f".{args.name}-init-{secrets.token_hex(8)}"
|
||||||
try:
|
try:
|
||||||
os.mkdir(candidate, mode=0o700, dir_fd=docs_fd)
|
os.mkdir(candidate, mode=0o700, dir_fd=pouch_fd)
|
||||||
except FileExistsError:
|
except FileExistsError:
|
||||||
continue
|
continue
|
||||||
transaction_name = candidate
|
transaction_name = candidate
|
||||||
@@ -1531,7 +1532,7 @@ def cmd_init(args: argparse.Namespace) -> None:
|
|||||||
transaction_fd = os.open(
|
transaction_fd = os.open(
|
||||||
transaction_name,
|
transaction_name,
|
||||||
os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW,
|
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)
|
os.mkdir("payload", mode=0o755, dir_fd=transaction_fd)
|
||||||
staging_fd = os.open(
|
staging_fd = os.open(
|
||||||
@@ -1560,21 +1561,21 @@ def cmd_init(args: argparse.Namespace) -> None:
|
|||||||
phase="发布",
|
phase="发布",
|
||||||
)
|
)
|
||||||
_assert_open_directory_path(
|
_assert_open_directory_path(
|
||||||
docs_fd,
|
pouch_fd,
|
||||||
project / "docs",
|
project / ".pouch",
|
||||||
phase="发布",
|
phase="发布",
|
||||||
label="docs 目录",
|
label=".pouch 目录",
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
_rename_directory_noreplace(
|
_rename_directory_noreplace(
|
||||||
transaction_fd,
|
transaction_fd,
|
||||||
"payload",
|
"payload",
|
||||||
docs_fd,
|
pouch_fd,
|
||||||
args.name,
|
args.name,
|
||||||
)
|
)
|
||||||
except FileExistsError as exc:
|
except FileExistsError as exc:
|
||||||
raise SystemExit(
|
raise SystemExit(
|
||||||
f"拒绝覆盖已有路径: docs/{args.name}"
|
f"拒绝覆盖已有路径: .pouch/{args.name}"
|
||||||
) from exc
|
) from exc
|
||||||
published = True
|
published = True
|
||||||
_assert_open_directory_path(
|
_assert_open_directory_path(
|
||||||
@@ -1586,23 +1587,23 @@ def cmd_init(args: argparse.Namespace) -> None:
|
|||||||
if (
|
if (
|
||||||
transaction_name is not None
|
transaction_name is not None
|
||||||
and _directory_entry_matches_open_fd(
|
and _directory_entry_matches_open_fd(
|
||||||
docs_fd,
|
pouch_fd,
|
||||||
transaction_name,
|
transaction_name,
|
||||||
transaction_fd,
|
transaction_fd,
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
os.rmdir(transaction_name, dir_fd=docs_fd)
|
os.rmdir(transaction_name, dir_fd=pouch_fd)
|
||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
transaction_name = None
|
transaction_name = None
|
||||||
try:
|
try:
|
||||||
os.fsync(docs_fd)
|
os.fsync(pouch_fd)
|
||||||
except OSError as exc:
|
except OSError as exc:
|
||||||
raise SystemExit(
|
raise SystemExit(
|
||||||
"初始化目录已完整发布,但无法确认目录项持久化;"
|
"初始化目录已完整发布,但无法确认目录项持久化;"
|
||||||
f"请检查 docs/{args.name} 后再重试"
|
f"请检查 .pouch/{args.name} 后再重试"
|
||||||
) from exc
|
) from exc
|
||||||
_assert_open_directory_path(
|
_assert_open_directory_path(
|
||||||
project_fd,
|
project_fd,
|
||||||
@@ -1610,10 +1611,10 @@ def cmd_init(args: argparse.Namespace) -> None:
|
|||||||
phase="完成初始化",
|
phase="完成初始化",
|
||||||
)
|
)
|
||||||
_assert_open_directory_path(
|
_assert_open_directory_path(
|
||||||
docs_fd,
|
pouch_fd,
|
||||||
project / "docs",
|
project / ".pouch",
|
||||||
phase="完成初始化",
|
phase="完成初始化",
|
||||||
label="docs 目录",
|
label=".pouch 目录",
|
||||||
)
|
)
|
||||||
_assert_open_directory_path(
|
_assert_open_directory_path(
|
||||||
staging_fd,
|
staging_fd,
|
||||||
@@ -1638,15 +1639,15 @@ def cmd_init(args: argparse.Namespace) -> None:
|
|||||||
if (
|
if (
|
||||||
transaction_name is not None
|
transaction_name is not None
|
||||||
and transaction_fd 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(
|
and _directory_entry_matches_open_fd(
|
||||||
docs_fd,
|
pouch_fd,
|
||||||
transaction_name,
|
transaction_name,
|
||||||
transaction_fd,
|
transaction_fd,
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
os.rmdir(transaction_name, dir_fd=docs_fd)
|
os.rmdir(transaction_name, dir_fd=pouch_fd)
|
||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
raise
|
raise
|
||||||
@@ -1654,7 +1655,7 @@ def cmd_init(args: argparse.Namespace) -> None:
|
|||||||
for directory_fd in (
|
for directory_fd in (
|
||||||
staging_fd,
|
staging_fd,
|
||||||
transaction_fd,
|
transaction_fd,
|
||||||
docs_fd,
|
pouch_fd,
|
||||||
project_fd,
|
project_fd,
|
||||||
):
|
):
|
||||||
if directory_fd is not None:
|
if directory_fd is not None:
|
||||||
@@ -1697,20 +1698,20 @@ def _add_common_flags(parser: argparse.ArgumentParser) -> None:
|
|||||||
|
|
||||||
def build_parser() -> argparse.ArgumentParser:
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
prog="skiff",
|
prog="pouch",
|
||||||
description="自研 Agent Skills 安装与管理 CLI(接口对齐 Vercel skills)",
|
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)
|
sub = parser.add_subparsers(dest="command", required=True)
|
||||||
|
|
||||||
p_bootstrap = sub.add_parser(
|
p_bootstrap = sub.add_parser(
|
||||||
"bootstrap",
|
"bootstrap",
|
||||||
help="将本项目 skiff skill 全局安装到所有 agent",
|
help="将本项目 pouch skill 全局安装到所有 agent",
|
||||||
)
|
)
|
||||||
p_bootstrap.set_defaults(func=cmd_bootstrap)
|
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_update.set_defaults(func=cmd_update)
|
||||||
|
|
||||||
p_list = sub.add_parser("list", help="列出所有 source 中的 skill")
|
p_list = sub.add_parser("list", help="列出所有 source 中的 skill")
|
||||||
@@ -1754,7 +1755,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
|
|
||||||
p_publish = sub.add_parser(
|
p_publish = sub.add_parser(
|
||||||
"publish",
|
"publish",
|
||||||
help="在 ~/.skills 内 git add / commit / push",
|
help="在 ~/.pouch 内 git add / commit / push",
|
||||||
)
|
)
|
||||||
p_publish.add_argument("paths", nargs="*", help="要提交的路径(默认 .)")
|
p_publish.add_argument("paths", nargs="*", help="要提交的路径(默认 .)")
|
||||||
p_publish.add_argument("-m", "--message", help="commit 说明")
|
p_publish.add_argument("-m", "--message", help="commit 说明")
|
||||||
@@ -1805,11 +1806,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
p_source_remove.add_argument(
|
p_source_remove.add_argument(
|
||||||
"--delete-checkout",
|
"--delete-checkout",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="同时永久删除 skiff 管理的 checkout",
|
help="同时永久删除 pouch 管理的 checkout",
|
||||||
)
|
)
|
||||||
p_source_remove.set_defaults(func=cmd_source_remove)
|
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("-a", "--agent", dest="agents", nargs="+", action="append")
|
||||||
p_sync.add_argument("--project")
|
p_sync.add_argument("--project")
|
||||||
p_sync.set_defaults(func=cmd_sync)
|
p_sync.set_defaults(func=cmd_sync)
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
"""~/.skills 仓库内的 git 操作。"""
|
"""~/.pouch 仓库内的 git 操作。"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -6,7 +6,7 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
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]:
|
def _run_git(repo: Path, *args: str, check: bool = True) -> subprocess.CompletedProcess[str]:
|
||||||
@@ -35,7 +35,7 @@ def publish(
|
|||||||
push: bool,
|
push: bool,
|
||||||
no_commit: bool,
|
no_commit: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
repo = SKILLS_HOME.resolve()
|
repo = POUCH_HOME.resolve()
|
||||||
ensure_git_repo(repo)
|
ensure_git_repo(repo)
|
||||||
|
|
||||||
_run_git(repo, "add", "--", *paths)
|
_run_git(repo, "add", "--", *paths)
|
||||||
+103
@@ -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"
|
||||||
|
)
|
||||||
@@ -1,22 +1,22 @@
|
|||||||
"""项目级 .skills.yaml 管理。"""
|
"""项目级 .pouch.yaml 管理。"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from skiff import yaml_io
|
from pouch import yaml_io
|
||||||
from skiff.paths import PROJECT_MANIFEST
|
from pouch.paths import project_manifest
|
||||||
from skiff.skills import normalize_source, resolve_skill_source
|
from pouch.skills import normalize_source, resolve_skill_source
|
||||||
|
|
||||||
|
|
||||||
def load_manifest(path: Path | None = None) -> tuple[Path, dict[str, Any]]:
|
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():
|
if not path.is_file():
|
||||||
return path, {"skills": []}
|
return path, {"skills": []}
|
||||||
data = yaml_io.safe_load(path.read_text(encoding="utf-8")) or {}
|
data = yaml_io.safe_load(path.read_text(encoding="utf-8")) or {}
|
||||||
if not isinstance(data, dict):
|
if not isinstance(data, dict):
|
||||||
raise SystemExit(f".skills.yaml 格式错误: {path}")
|
raise SystemExit(f".pouch.yaml 格式错误: {path}")
|
||||||
if "skills" not in data:
|
if "skills" not in data:
|
||||||
data["skills"] = []
|
data["skills"] = []
|
||||||
return path, data
|
return path, data
|
||||||
@@ -34,7 +34,7 @@ def normalize_skill_entry(entry: str | dict[str, Any]) -> dict[str, Any]:
|
|||||||
return {"name": entry, "source": "builtin"}
|
return {"name": entry, "source": "builtin"}
|
||||||
name = entry.get("name")
|
name = entry.get("name")
|
||||||
if not 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")))
|
source = normalize_source(str(entry.get("source", "builtin")))
|
||||||
if source == "catalog" and entry.get("registry"):
|
if source == "catalog" and entry.get("registry"):
|
||||||
source = f"catalog:{entry['registry']}"
|
source = f"catalog:{entry['registry']}"
|
||||||
@@ -58,7 +58,7 @@ def add_skill_to_manifest(
|
|||||||
) -> None:
|
) -> None:
|
||||||
path = manifest_path
|
path = manifest_path
|
||||||
if path.is_dir():
|
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": []})
|
file_path, data = load_manifest(path) if path.is_file() else (path, {"skills": []})
|
||||||
if not path.is_file():
|
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:
|
def remove_skill_from_manifest(manifest_path: Path, name: str) -> bool:
|
||||||
path = manifest_path
|
path = manifest_path
|
||||||
if not path.is_file():
|
if path.is_dir() or not path.is_file():
|
||||||
path = path / PROJECT_MANIFEST
|
path = project_manifest(path if path.is_dir() else path.parent)
|
||||||
file_path, data = load_manifest(path)
|
file_path, data = load_manifest(path)
|
||||||
original = data.get("skills", [])
|
original = data.get("skills", [])
|
||||||
kept = [e for e in original if normalize_skill_entry(e)["name"] != name]
|
kept = [e for e in original if normalize_skill_entry(e)["name"] != name]
|
||||||
@@ -165,7 +165,7 @@ def select_skills(
|
|||||||
)
|
)
|
||||||
|
|
||||||
footer = (
|
footer = (
|
||||||
f"已选择 {len(selected)} 项 · 取消选择不卸载,卸载用 skiff remove"
|
f"已选择 {len(selected)} 项 · 取消选择不卸载,卸载用 pouch remove"
|
||||||
)
|
)
|
||||||
stdscr.addnstr(
|
stdscr.addnstr(
|
||||||
height - 1,
|
height - 1,
|
||||||
@@ -5,13 +5,13 @@ from __future__ import annotations
|
|||||||
import re
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from skiff.paths import SKILLS_DIR, TEMPLATE_DIR, ensure_skills_home
|
from pouch.paths import SKILLS_DIR, TEMPLATE_DIR, ensure_pouch_home
|
||||||
from skiff.catalog import (
|
from pouch.catalog import (
|
||||||
catalog_skill_path,
|
catalog_skill_path,
|
||||||
discover_catalog_skills,
|
discover_catalog_skills,
|
||||||
load_catalog,
|
load_catalog,
|
||||||
)
|
)
|
||||||
from skiff.sources import (
|
from pouch.sources import (
|
||||||
discover_source_skills,
|
discover_source_skills,
|
||||||
list_source_skills,
|
list_source_skills,
|
||||||
load_sources,
|
load_sources,
|
||||||
@@ -19,7 +19,7 @@ from skiff.sources import (
|
|||||||
|
|
||||||
|
|
||||||
def list_builtin_skills() -> list[str]:
|
def list_builtin_skills() -> list[str]:
|
||||||
ensure_skills_home()
|
ensure_pouch_home()
|
||||||
if not SKILLS_DIR.is_dir():
|
if not SKILLS_DIR.is_dir():
|
||||||
return []
|
return []
|
||||||
names: list[str] = []
|
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]:
|
def resolve_skill_source(name: str, *, source: str | None = None) -> tuple[Path, str]:
|
||||||
"""返回 (skill_path, source),未指定来源时拒绝同名歧义。"""
|
"""返回 (skill_path, source),未指定来源时拒绝同名歧义。"""
|
||||||
ensure_skills_home()
|
ensure_pouch_home()
|
||||||
name, source = split_skill_spec(name, source)
|
name, source = split_skill_spec(name, source)
|
||||||
|
|
||||||
builtin = SKILLS_DIR / name
|
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():
|
if not (path / "SKILL.md").is_file():
|
||||||
raise SystemExit(
|
raise SystemExit(
|
||||||
f"catalog skill {name!r} 尚未 fetch 或 path 中缺少 SKILL.md。"
|
f"catalog skill {name!r} 尚未 fetch 或 path 中缺少 SKILL.md。"
|
||||||
f"请运行: skiff fetch {name}"
|
f"请运行: pouch fetch {name}"
|
||||||
)
|
)
|
||||||
return path, f"catalog:{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:
|
if source not in sources:
|
||||||
raise SystemExit(
|
raise SystemExit(
|
||||||
f"项目依赖 source {source!r},但本机尚未配置。"
|
f"项目依赖 source {source!r},但本机尚未配置。"
|
||||||
f"请运行: skiff source add {source} <repo>"
|
f"请运行: pouch source add {source} <repo>"
|
||||||
)
|
)
|
||||||
skills = discover_source_skills(source, sources[source])
|
skills = discover_source_skills(source, sources[source])
|
||||||
if name not in skills:
|
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]
|
path, resolved_source = candidates[0]
|
||||||
if resolved_source.startswith("catalog:") and not path.exists():
|
if resolved_source.startswith("catalog:") and not path.exists():
|
||||||
provider = resolved_source.split(":", 1)[1]
|
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
|
return path, resolved_source
|
||||||
|
|
||||||
raise SystemExit(f"找不到 skill: {name}")
|
raise SystemExit(f"找不到 skill: {name}")
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## 背景
|
## 背景
|
||||||
|
|
||||||
skiff 当前使用 `owned` 表示本仓库 `skills/` 中维护的 Skill,同时还支持
|
pouch 当前使用 `owned` 表示本仓库 `skills/` 中维护的 Skill,同时还支持
|
||||||
`registry` 外部仓库和用户配置的 custom source。
|
`registry` 外部仓库和用户配置的 custom source。
|
||||||
|
|
||||||
这些名称混合了不同维度:
|
这些名称混合了不同维度:
|
||||||
@@ -17,8 +17,8 @@ skiff 当前使用 `owned` 表示本仓库 `skills/` 中维护的 Skill,同时
|
|||||||
|
|
||||||
本文建议将用户可见的来源注册方式统一为三类:
|
本文建议将用户可见的来源注册方式统一为三类:
|
||||||
|
|
||||||
- `builtin`:随当前 skiff 仓库提供的 Skill。
|
- `builtin`:随当前 pouch 仓库提供的 Skill。
|
||||||
- `catalog:<name>`:由 skiff 自带目录预先登记的来源。
|
- `catalog:<name>`:由 pouch 自带目录预先登记的来源。
|
||||||
- `custom:<name>`:用户在本机显式配置的命名来源。
|
- `custom:<name>`:用户在本机显式配置的命名来源。
|
||||||
|
|
||||||
来源注册方式与获取方式、仓库布局相互独立。builtin、catalog 和 custom 中的任意
|
来源注册方式与获取方式、仓库布局相互独立。builtin、catalog 和 custom 中的任意
|
||||||
@@ -29,20 +29,20 @@ Git 仓库或本地目录。
|
|||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart TD
|
flowchart TD
|
||||||
S[skiff 可发现的 Skills]
|
S[pouch 可发现的 Skills]
|
||||||
|
|
||||||
S --> O["owned<br/>~/.skills/skills/*"]
|
S --> O["owned<br/>~/.pouch/skills/*"]
|
||||||
S --> C["custom source<br/>~/.config/skiff/config.yaml"]
|
S --> C["custom source<br/>~/.config/pouch/config.yaml"]
|
||||||
S --> R["registry<br/>registry.yaml"]
|
S --> R["registry<br/>registry.yaml"]
|
||||||
|
|
||||||
O --> O1["本仓库维护<br/>随 skiff 一起分发"]
|
O --> O1["本仓库维护<br/>随 pouch 一起分发"]
|
||||||
|
|
||||||
C --> C1["本地目录<br/>--local PATH"]
|
C --> C1["本地目录<br/>--local PATH"]
|
||||||
C --> C2["指定 Git 仓库<br/>repo + skills_path"]
|
C --> C2["指定 Git 仓库<br/>repo + skills_path"]
|
||||||
|
|
||||||
R --> R1["外部单 Skill 仓库"]
|
R --> R1["外部单 Skill 仓库"]
|
||||||
R --> R2["外部 Skill Collection"]
|
R --> R2["外部 Skill Collection"]
|
||||||
R1 --> E["~/.local/share/skills/externals/"]
|
R1 --> E["~/.local/share/pouch/externals/"]
|
||||||
R2 --> E
|
R2 --> E
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ flowchart TD
|
|||||||
- `list` 和 `status` 支持 owned、registry 和 custom source。
|
- `list` 和 `status` 支持 owned、registry 和 custom source。
|
||||||
- `resolve_skill_source` 可以解析三种来源并处理同名歧义。
|
- `resolve_skill_source` 可以解析三种来源并处理同名歧义。
|
||||||
- `select` 只组装 owned 和 registry 条目,尚未展示 custom source。
|
- `select` 只组装 owned 和 registry 条目,尚未展示 custom source。
|
||||||
- `.skills.yaml` 默认将未声明来源的 Skill 解释为 `owned`。
|
- `.pouch.yaml` 默认将未声明来源的 Skill 解释为 `owned`。
|
||||||
- custom source 的 `skills_path` 已经可以包含多个 Skill,本质上也是 collection。
|
- custom source 的 `skills_path` 已经可以包含多个 Skill,本质上也是 collection。
|
||||||
|
|
||||||
## 现行模型
|
## 现行模型
|
||||||
@@ -61,11 +61,11 @@ flowchart TD
|
|||||||
A[Skill Provider]
|
A[Skill Provider]
|
||||||
|
|
||||||
A --> B["builtin<br/>仓库内隐式注册"]
|
A --> B["builtin<br/>仓库内隐式注册"]
|
||||||
A --> C["catalog:waza<br/>skiff 预置目录"]
|
A --> C["catalog:waza<br/>pouch 预置目录"]
|
||||||
A --> D["custom:company<br/>用户本机配置"]
|
A --> D["custom:company<br/>用户本机配置"]
|
||||||
|
|
||||||
B --> B1["skills/ack"]
|
B --> B1["skills/ack"]
|
||||||
B --> B2["skills/skiff"]
|
B --> B2["skills/pouch"]
|
||||||
B --> B3["skills/builder"]
|
B --> B3["skills/builder"]
|
||||||
|
|
||||||
C --> C1["Git 或本地目录"]
|
C --> C1["Git 或本地目录"]
|
||||||
@@ -85,14 +85,14 @@ flowchart TD
|
|||||||
|
|
||||||
| 类型 | 含义 | 配置来源 | 用户界面展示 |
|
| 类型 | 含义 | 配置来源 | 用户界面展示 |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| `builtin` | 随当前 skiff 仓库提供 | `skills/` | `builtin` |
|
| `builtin` | 随当前 pouch 仓库提供 | `skills/` | `builtin` |
|
||||||
| `catalog` | skiff 预先登记、所有用户可发现的来源 | `catalog.yaml` | `catalog:<name>` |
|
| `catalog` | pouch 预先登记、所有用户可发现的来源 | `catalog.yaml` | `catalog:<name>` |
|
||||||
| `custom` | 用户在本机显式注册的命名来源 | `~/.config/skiff/config.yaml` | `custom:<name>` |
|
| `custom` | 用户在本机显式注册的命名来源 | `~/.config/pouch/config.yaml` | `custom:<name>` |
|
||||||
|
|
||||||
`builtin` 比 `owned` 更适合作为用户可见名称,因为它表达 Skill 的分发位置和可用
|
`builtin` 比 `owned` 更适合作为用户可见名称,因为它表达 Skill 的分发位置和可用
|
||||||
方式,而不是仓库的所有权关系。
|
方式,而不是仓库的所有权关系。
|
||||||
|
|
||||||
`catalog` 比 `registry` 更准确:当前文件只是 skiff 随仓库维护的预置来源目录,
|
`catalog` 比 `registry` 更准确:当前文件只是 pouch 随仓库维护的预置来源目录,
|
||||||
并不是远程注册中心,也不是一种 Skill 来源协议。
|
并不是远程注册中心,也不是一种 Skill 来源协议。
|
||||||
|
|
||||||
### 三个正交维度
|
### 三个正交维度
|
||||||
@@ -128,7 +128,7 @@ layout: single | collection
|
|||||||
|
|
||||||
## `select` 展示
|
## `select` 展示
|
||||||
|
|
||||||
`skiff select` 应同时展示 builtin、catalog 和 custom Skill:
|
`pouch select` 应同时展示 builtin、catalog 和 custom Skill:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
[ ] ack builtin
|
[ ] ack builtin
|
||||||
@@ -151,7 +151,7 @@ layout: single | collection
|
|||||||
|
|
||||||
## 配置表示
|
## 配置表示
|
||||||
|
|
||||||
新写入的 `.skills.yaml` 使用以下形式:
|
新写入的 `.pouch.yaml` 使用以下形式:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
skills:
|
skills:
|
||||||
@@ -172,7 +172,7 @@ custom source 在 manifest 中继续保存其逻辑名称,例如 `company`。
|
|||||||
|
|
||||||
1. 对外文档、CLI 输出和 selector 统一使用 `builtin`、`catalog:<name>` 和
|
1. 对外文档、CLI 输出和 selector 统一使用 `builtin`、`catalog:<name>` 和
|
||||||
`custom:<name>`。
|
`custom:<name>`。
|
||||||
2. 新生成的 `.skills.yaml` 对内置 Skill 写入 `source: builtin`。
|
2. 新生成的 `.pouch.yaml` 对内置 Skill 写入 `source: builtin`。
|
||||||
3. 读取旧 manifest 时继续接受 `source: owned`,并在解析时归一化为 `builtin`。
|
3. 读取旧 manifest 时继续接受 `source: owned`,并在解析时归一化为 `builtin`。
|
||||||
4. CLI 参数在过渡期继续接受 `--source owned`,但帮助和输出只推荐 `builtin`。
|
4. CLI 参数在过渡期继续接受 `--source owned`,但帮助和输出只推荐 `builtin`。
|
||||||
5. 读取旧 manifest 中的 `source: registry` 和 `registry: <name>`,归一化为
|
5. 读取旧 manifest 中的 `source: registry` 和 `registry: <name>`,归一化为
|
||||||
@@ -188,14 +188,14 @@ custom source 在 manifest 中继续保存其逻辑名称,例如 `company`。
|
|||||||
|
|
||||||
当前实现覆盖:
|
当前实现覆盖:
|
||||||
|
|
||||||
- `skiff/skills.py`:来源解析、归一化和 builtin 命名。
|
- `pouch/skills.py`:来源解析、归一化和 builtin 命名。
|
||||||
- `skiff/project.py`:manifest 默认值、序列化与旧值兼容。
|
- `pouch/project.py`:manifest 默认值、序列化与旧值兼容。
|
||||||
- `skiff/sources.py`:来源保留字。
|
- `pouch/sources.py`:来源保留字。
|
||||||
- `skiff/catalog.py`:catalog 配置、checkout 与 Skill 发现。
|
- `pouch/catalog.py`:catalog 配置、checkout 与 Skill 发现。
|
||||||
- `skiff/cli.py`:`list`、`status`、`add`、`select` 和输出文案。
|
- `pouch/cli.py`:`list`、`status`、`add`、`select` 和输出文案。
|
||||||
- `skiff/selector.py`:统一 catalog/custom collection 的父子展示。
|
- `pouch/selector.py`:统一 catalog/custom collection 的父子展示。
|
||||||
- CLI 与来源解析测试。
|
- 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`。如果未来
|
发布、版本解析或可信签名等注册中心能力,因此现已改为 `catalog.yaml`。如果未来
|
||||||
实现真正的远程 registry,应单独定义其协议和与 catalog 的同步关系,不复用旧名称。
|
实现真正的远程 registry,应单独定义其协议和与 catalog 的同步关系,不复用旧名称。
|
||||||
@@ -6,14 +6,14 @@ import subprocess
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from skiff import yaml_io
|
from pouch import yaml_io
|
||||||
from skiff.paths import CONFIG_FILE, SOURCES_DIR
|
from pouch.paths import CONFIG_FILE, SOURCES_DIR
|
||||||
|
|
||||||
RESERVED_SOURCES = {"builtin", "catalog", "owned", "registry"}
|
RESERVED_SOURCES = {"builtin", "catalog", "owned", "registry"}
|
||||||
|
|
||||||
|
|
||||||
def validate_source_name(name: str) -> None:
|
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)
|
validate_skill_name(name)
|
||||||
if name in RESERVED_SOURCES:
|
if name in RESERVED_SOURCES:
|
||||||
@@ -26,12 +26,12 @@ def load_sources(path: Path | None = None) -> dict[str, dict[str, Any]]:
|
|||||||
return {}
|
return {}
|
||||||
data = yaml_io.safe_load(path.read_text(encoding="utf-8")) or {}
|
data = yaml_io.safe_load(path.read_text(encoding="utf-8")) or {}
|
||||||
if not isinstance(data, dict):
|
if not isinstance(data, dict):
|
||||||
raise SystemExit(f"skiff 配置格式错误: {path}")
|
raise SystemExit(f"pouch 配置格式错误: {path}")
|
||||||
raw = data.get("sources", {})
|
raw = data.get("sources", {})
|
||||||
if raw is None:
|
if raw is None:
|
||||||
return {}
|
return {}
|
||||||
if not isinstance(raw, dict):
|
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)}
|
return {str(name): entry for name, entry in raw.items() if isinstance(entry, dict)}
|
||||||
|
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ def copy_template(src: Path, dst: Path) -> None:
|
|||||||
def find_repo_root(start: Path | None = None) -> Path | None:
|
def find_repo_root(start: Path | None = None) -> Path | None:
|
||||||
start = (start or Path.cwd()).resolve()
|
start = (start or Path.cwd()).resolve()
|
||||||
for directory in [start, *start.parents]:
|
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
|
return directory
|
||||||
if (directory / "skills").is_dir() and (
|
if (directory / "skills").is_dir() and (
|
||||||
(directory / "catalog.yaml").is_file()
|
(directory / "catalog.yaml").is_file()
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
"""轻量 YAML 读写(覆盖 skiff 使用的子集,无第三方依赖)。"""
|
"""轻量 YAML 读写(覆盖 pouch 使用的子集,无第三方依赖)。"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
+1
-1
@@ -1 +1 @@
|
|||||||
# skiff 仅使用 Python 标准库,无第三方依赖
|
# pouch 仅使用 Python 标准库,无第三方依赖
|
||||||
|
|||||||
-253
@@ -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)
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
"""skiff — Agent Skills 安装与管理 CLI。"""
|
|
||||||
|
|
||||||
__version__ = "0.6.0"
|
|
||||||
@@ -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,11 +27,38 @@ description: >-
|
|||||||
2. 第二步
|
2. 第二步
|
||||||
3. 第三步
|
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
@@ -1,30 +1,27 @@
|
|||||||
# ACK
|
# ACK
|
||||||
|
|
||||||
ACK 是一个显式调用的 Agent Skill,用三种独立角色运行工程协作闭环:
|
ACK 是显式调用的 Agent Skill,用三种独立角色跑工程协作闭环:
|
||||||
|
|
||||||
- Coordinator 拆解需求、派发任务并终检。
|
- Coordinator 拆解需求、派发任务并终检
|
||||||
- Developer 实现并执行白盒验证。
|
- Developer 实现并做白盒验证
|
||||||
- Test 独立执行黑盒复测。
|
- Test 独立做黑盒复测
|
||||||
|
|
||||||
关键约束是验证者不等于实现者。每个任务最多修复三轮,仍未通过时记录为
|
验证者不能等于实现者。每个任务最多修三轮,仍不过就记为 `leftover`,然后继续别的任务。
|
||||||
`leftover`,然后继续处理其它任务。
|
|
||||||
|
|
||||||
测试环境部署由 ACK 触发、**内部调用 deployer** 执行。发版仍写在同一份
|
测试环境由 ACK 触发、内部调用 [deployer](../deployer/README.md)。发版写在同一份 `.pouch/ack/delivery.yaml`。功能或 bug 验证通过后,黑盒用例收进 `.pouch/ack/regression.yaml`,之后可以单独跑回归。
|
||||||
`docs/ack/delivery.yaml`。功能或 bug 验证通过后,把黑盒用例收进
|
|
||||||
`docs/ack/regression.yaml`;之后可以单独跑回归。
|
|
||||||
|
|
||||||
ACK 只在用户显式调用 `/ack` 或 `$ack` 时运行。
|
只在你显式说 `/ack` 或 `$ack` 时运行。
|
||||||
|
|
||||||
## 使用场景
|
## 什么时候使用
|
||||||
|
|
||||||
| 场景 | 怎么说 | 结果 |
|
| 场景 | 怎么说 | 结果 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| 初始化 | `/ack 初始化` | 生成并补全 `docs/ack/` |
|
| 初始化 | `/ack 初始化` | 生成并补全 `.pouch/ack/`;缺项按统一格式列出 |
|
||||||
| 检查 | `/ack 检查配置` | 只读校验,默认不改文件 |
|
| 检查 | `/ack 检查配置` | 只读校验,默认不改文件;测试环境缺 deployer 时转交 |
|
||||||
| 做需求 | `/ack 处理这个需求:…` | 产品文档 + 拆任务 → 确认 → 三角色闭环 |
|
| 做需求 | `/ack 处理这个需求:…` | 产品文档 + 拆任务 → 你确认 → 三角色闭环 |
|
||||||
| 修 bug | `/ack 修这个 bug:…` 或处理飞书收件 | 短描述 + 验收 → 确认(飞书须你点「已确认」)→ 同一闭环 |
|
| 修 bug | `/ack 修这个 bug:…` 或处理飞书收件 | 短描述 + 验收 → 你确认(飞书须你点「已确认」)→ 同一闭环 |
|
||||||
| 交付配置 | 说明怎么布测试环境 / 怎么发版 | 写入同一份 `delivery.yaml`;测试环境绑定 deployer |
|
| 交付配置 | 说明怎么布测试环境 / 怎么发版 | 写入同一份 `delivery.yaml`;测试环境绑定 deployer |
|
||||||
| 运行测试环境 | `/ack 重新布测试环境` | 内部加载 deployer,布 `.skiff/deployer/<env>` |
|
| 运行测试环境 | `/ack 重新布测试环境` | 内部加载 deployer,布 `.pouch/deployer/<env>` |
|
||||||
| 运行版本发布 | `/ack 发布一个版本` | 按 `intents.release`;stable/生产仍要单独批准 |
|
| 运行版本发布 | `/ack 发布一个版本` | 按 `intents.release`;stable/生产仍要单独批准 |
|
||||||
| 回归 | `/ack 回归` | 先布测试环境,再按 `regression.yaml` 用浏览器或 API 跑 |
|
| 回归 | `/ack 回归` | 先布测试环境,再按 `regression.yaml` 用浏览器或 API 跑 |
|
||||||
|
|
||||||
@@ -32,29 +29,30 @@ ACK 只在用户显式调用 `/ack` 或 `$ack` 时运行。
|
|||||||
|
|
||||||
## 安装
|
## 安装
|
||||||
|
|
||||||
全局安装:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
skiff add ack -g
|
pouch add ack # 当前项目
|
||||||
|
pouch add ack -g # 全局
|
||||||
```
|
```
|
||||||
|
|
||||||
或只安装到当前项目:
|
## 使用前准备
|
||||||
|
|
||||||
```bash
|
- 项目是 Git 仓库。
|
||||||
skiff add ack
|
- 自动拉起 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
|
```bash
|
||||||
skiff init ack
|
pouch init ack
|
||||||
skiff init ack --project ~/code/my-app
|
pouch init ack --project ~/code/my-app
|
||||||
```
|
```
|
||||||
|
|
||||||
初始化后,项目只保存自己的 ACK 状态:
|
也可以直接说 `/ack 初始化`。初始化后项目只保存自己的状态:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
docs/ack/
|
.pouch/ack/
|
||||||
├── project.md
|
├── project.md
|
||||||
├── tasks.yaml
|
├── tasks.yaml
|
||||||
├── knowledge.yaml
|
├── knowledge.yaml
|
||||||
@@ -62,269 +60,62 @@ docs/ack/
|
|||||||
└── regression.yaml # 默认 cases: []
|
└── regression.yaml # 默认 cases: []
|
||||||
```
|
```
|
||||||
|
|
||||||
不会在项目中复制或链接 ACK Skill。通用规范、模板和脚本始终从已安装的 Skill
|
不会把 ACK Skill 复制进项目。规范、模板和脚本始终从已安装的 Skill 目录读取。
|
||||||
目录读取。
|
|
||||||
|
|
||||||
ACK 从当前命令指定的 `--project-root/docs/ack/` 定位项目状态,不要求在
|
旧项目如果只有 `project.md` 和 `tasks.yaml`,不要重跑初始化。说 `/ack 检查配置`,授权后再补空的 `knowledge.yaml` 等缺件。
|
||||||
`tasks.yaml` 中持久化 `repoPath` 或 `devWorktree`。自动 worker 的实际工作目录由
|
|
||||||
`--worktree` 指定;默认在 `--project-root` 工作,v0.19 起不再配置 `allowedWorktrees` 白名单。
|
|
||||||
|
|
||||||
## 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
|
```text
|
||||||
|
/ack 初始化
|
||||||
|
/ack 检查配置
|
||||||
|
/ack 处理这个需求:给后台加导出 CSV
|
||||||
|
/ack 修这个 bug:保存后列表不刷新。复现:… 期望:…
|
||||||
|
/ack 处理飞书里待整理的 bug
|
||||||
|
/ack 测试环境用项目里的 .pouch/deployer/test;发版方式以后再告诉你
|
||||||
/ack 重新布一下测试环境,我要测试
|
/ack 重新布一下测试环境,我要测试
|
||||||
/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 回归
|
||||||
/ack 跑 full 回归
|
/ack 跑 full 回归
|
||||||
/ack 回归 REG-login-001
|
/ack 回归 REG-login-001
|
||||||
|
/ack 修这些回归失败
|
||||||
```
|
```
|
||||||
|
|
||||||
ACK 先按 deployer 布测试环境,再派独立 Test 按用例用浏览器或 API 执行。失败只
|
首次配发版也可以一次说清:
|
||||||
报告,不会自动开修;要修再说 `/ack 修这些回归失败`。默认跑 smoke。细则见
|
|
||||||
`references/regression.md`。
|
|
||||||
|
|
||||||
首次配置交付可以说:
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
/ack 更新项目交付配置:用 make build-deb 构建 DEB,发布到 preview APT 仓库,
|
/ack 更新项目交付配置:用 make build-deb 构建 DEB,发布到 preview APT 仓库,
|
||||||
部署到 test-server 并跑健康检查;完成后创建 PR,停在 review_ready 给我审核。
|
部署到 test-server 并跑健康检查;完成后创建 PR,停在 review_ready 给我审核。
|
||||||
```
|
```
|
||||||
|
|
||||||
之后处理需求时只需在确认计划中选择 profile:
|
之后做需求时只要在确认计划里选 profile:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
/ack 处理这个需求:<一句话需求>。任务验证通过后执行 review profile。
|
/ack 处理这个需求:…。任务验证通过后执行 review profile。
|
||||||
```
|
```
|
||||||
|
|
||||||
ACK 会自动读取 `delivery.yaml`,无需再逐步提醒它构建、上传、部署或开 PR;目标或
|
## Agent 会做什么
|
||||||
权限发生漂移、缺少凭据、进入 stable/production approval 时才停下来请求决策。
|
|
||||||
|
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 记录
|
当前 Skill 版本见 `VERSION`。新项目在 `tasks.yaml` 里记 `ackVersion`。
|
||||||
`ackVersion`。从 `0.10.0` 起,`project.orchestration` 与顶层 `workerReceipts` 必须
|
|
||||||
同时存在;从 `0.11.0` 起,新项目还会生成默认关闭的 `delivery.yaml`,并在任务板声明
|
从 0.19.0 起,测试环境写成 `{via: deployer, env: <env>}`,回归目录是 `.pouch/ack/regression.yaml`。旧的测试环境 profile ID 字符串不再执行,需要迁过去。旧项目可以不迁回归目录,继续用原来的闭环。旧的 `kitVersion` 仍可读,建议改成 `ackVersion`。
|
||||||
`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`。
|
|
||||||
|
|||||||
+92
-311
@@ -1,325 +1,106 @@
|
|||||||
---
|
---
|
||||||
name: ack
|
name: ack
|
||||||
description: >-
|
description: >-
|
||||||
初始化、检查并运行 ACK 三角色协作闭环。仅在用户显式调用 /ack 或 $ack,并要求
|
初始化、检查并运行 ACK 三角色闭环。仅在用户显式调用 /ack 或 $ack,并要求
|
||||||
初始化 ACK、检查 docs/ack 配置、按 ACK 规划需求或修复 bug、指挥
|
初始化 ACK、检查 .pouch/ack、按 ACK 做需求或修 bug、指挥
|
||||||
Coordinator/Developer/Test 工作,配置测试环境与发版方式,重新部署测试环境
|
Coordinator/Developer/Test、配置或运行测试环境与发版、或跑回归时使用。
|
||||||
(内部调用 deployer),发布版本,或运行回归测试时使用。
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# ACK 项目协作入口
|
# ACK 项目协作入口
|
||||||
|
|
||||||
本 Skill 是 ACK 的完整能力包:`references/` 保存通用规范,`templates/` 保存项目
|
项目只在 `.pouch/ack/` 保存 `project.md`、`tasks.yaml`、`knowledge.yaml`、
|
||||||
状态模板,`scripts/` 保存校验工具。目标项目只在 `docs/ack/` 保存 `project.md`、
|
`delivery.yaml` 和 `regression.yaml`。不要复制 Skill 内容,不要修改 `AGENTS.md`、
|
||||||
`tasks.yaml`、`knowledge.yaml`、默认关闭的 `delivery.yaml` 和空的
|
`CLAUDE.md`。
|
||||||
`regression.yaml`,不要复制或链接 Skill 内容。
|
|
||||||
|
|
||||||
开始时解析当前 `SKILL.md` 所在目录,记为 `<ack-skill-dir>`。所有通用规范、模板和
|
开始时解析当前 `SKILL.md` 所在目录为 `<ack-skill-dir>`。优先
|
||||||
脚本都相对此目录访问,不依赖固定的全局安装路径。
|
`git rev-parse --show-toplevel` 作为项目根。
|
||||||
|
|
||||||
## 选择模式
|
## 选择模式
|
||||||
|
|
||||||
- 用户要求初始化、接入或安装 ACK:执行“初始化”。
|
- 初始化、接入 ACK:执行「初始化」。
|
||||||
- 用户要求检查 ACK 是否可用、配置是否完整:执行“检查”。
|
- 检查配置是否完整:执行「检查」。
|
||||||
- 用户要求用 ACK 做需求、修复 bug 或继续任务:执行“工作”。修 bug 不写大 PRD;
|
- 做需求、修 bug 或继续任务:执行「工作」。修 bug 不写大 PRD。
|
||||||
飞书收件仍走本模式。
|
- 增改关闭交付、说明怎么布测试环境或发版:执行「交付配置维护」。
|
||||||
- 用户用自然语言说明怎么部署测试环境、怎么发布版本,或要求增加、修改、关闭交付
|
- 部署或重布测试环境:执行「运行测试环境」(加载 deployer,不复制 compose)。
|
||||||
流程:执行“交付配置维护”。测试环境绑定 deployer,发版写在同一份
|
- 发布版本:执行「运行版本发布」。回归:执行「运行回归」。
|
||||||
`docs/ack/delivery.yaml`。
|
- 任务板使用 Orca 时,编排命令见 [orca-adapter.md](references/orca-adapter.md)。
|
||||||
- 用户要求部署、重新部署测试环境,或按已配置方式开始测试:执行“运行测试环境”。
|
|
||||||
内部加载 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` 任务。
|
|
||||||
|
|
||||||
## 边界
|
## 边界
|
||||||
|
|
||||||
- 不修改或追加任何项目 Agent 指令文件,包括 `AGENTS.md`。
|
- 不覆盖已有 `.pouch/ack`;除用户确认的任务或 delivery profile 外,不擅自提交、推送、创建终端、新 worktree、发布或部署。
|
||||||
- 不在项目中维护第二份 ACK 通用规范、模板或任务 schema。
|
- 不猜测命令、地址、worker handle 或模型名;无法确定写 `n/a`。
|
||||||
- 不猜测项目命令、服务地址、worker handle 或模型名称。
|
- 不把完整 `tasks.yaml` / `knowledge.yaml` / `regression.yaml` 注入上下文;用对应 `select_*.py`。写回前跑完整校验。
|
||||||
- 不把 full-access、bypass、Grok `--yolo` / bypassPermissions、关闭 sandbox
|
- 只有 Coordinator 写任务板、知识库、回归目录、`deliveryRuns` 和 `regressionRuns`。`delivery.yaml` 只在「交付配置维护」中改。见 [roles-and-permissions.md](references/roles-and-permissions.md)。
|
||||||
或项目内“授权”字段当成 v0.10 自动 worker 的合法配置;这些 CLI 绕过标志
|
- 知识检查只经 `run_verification.py` 的 registry ID;不把知识正文拼成 shell。
|
||||||
当前一律 fail closed。Grok worker 由 launcher 固定带 `--always-approve`,
|
- 自动 worker 禁止 full-access、bypass、关闭 sandbox、Grok `--yolo` / bypassPermissions。OMP `--approval-mode yolo` 只用于 OMP 审批(workspace-write → yolo,read-only → always-ask),禁止 `--auto-approve`,不适用于其它 backend。
|
||||||
仍必须带 sandbox。
|
- 无清理证明不复用 worker;无密钥 `receiptHash` 或 Orca live metadata 不能授权复用。当前 Orca 走 fresh。`indeterminate` / `reconcile required` 不直接重试。
|
||||||
- OMP worker 使用结构化 `--model`、`--thinking` 和 `--approval-mode` 参数。
|
- 产品失败才占三轮;环境失败写 `environmentIncidents`。见 [optimization-method.md](references/optimization-method.md) §4。
|
||||||
`--approval-mode yolo` 是 OMP worker 的审批模式,不是 CLI 绕过标志:规则层
|
|
||||||
直接允许并默认启用(workspace-write → yolo、read-only → always-ask);
|
## 初始化
|
||||||
仍禁止 `--auto-approve`,也不适用于 codex/cursor-agent/grok。
|
|
||||||
- 不把无密钥 `receiptHash` 或 Orca live metadata 当作旧终端的启动 attestation;
|
1. 确认 `pouch` 可执行。`.pouch/ack` 不存在则 `pouch init ack --project <project-root>`;已存在则不覆盖、转入「检查」。
|
||||||
没有可信空闲状态、配置匹配和历史消息清理证明时不复用既有 worker。
|
2. 按 [init-new-project.md](references/init-new-project.md) 完善项目状态。不从 README/CI 猜测并启用交付或知识;初始化 **不** 自动初始化 deployer 或 builder。
|
||||||
- launcher 返回 `indeterminate` 或 `reconcile required` 时,不直接重试;先按
|
3. 运行:
|
||||||
launch ID、外部 record 和 Orca live state 完成人工核对。
|
|
||||||
- 不覆盖已有 `docs/ack` 文件;除用户确认的 ACK 任务或 delivery profile 外,不擅自
|
```bash
|
||||||
提交、推送、创建终端、新 worktree、发布产物或部署。
|
python3 <ack-skill-dir>/scripts/validate_tasks.py .pouch/ack/tasks.yaml
|
||||||
- 只有 Coordinator 写 `tasks.yaml`、`knowledge.yaml`、`regression.yaml`、
|
python3 <ack-skill-dir>/scripts/validate_knowledge.py .pouch/ack/knowledge.yaml \
|
||||||
`deliveryRuns` 和 `regressionRuns`;Developer 与 Test 只读,只能通过回报提名
|
--tasks .pouch/ack/tasks.yaml
|
||||||
或验证。`delivery.yaml` 只在显式的交付配置维护中修改。
|
python3 <ack-skill-dir>/scripts/validate_delivery.py .pouch/ack/delivery.yaml \
|
||||||
- 不把知识正文或选择器输出拼成 shell;知识检查只能通过 `run_verification.py`
|
--tasks .pouch/ack/tasks.yaml --project-root <project-root>
|
||||||
按 registry ID 执行。不自动修改 `AGENTS.md`、`CLAUDE.md` 或其它 Agent 指令文件。
|
python3 <ack-skill-dir>/scripts/validate_regression.py .pouch/ack/regression.yaml \
|
||||||
- 不把完整 `tasks.yaml` 注入上下文;使用 `select_tasks.py` 获取有预算的项目与任务
|
--tasks .pouch/ack/tasks.yaml
|
||||||
视图,写回前仍运行完整任务板校验。
|
```
|
||||||
- 项目只保存 `docs/ack/project.md`、`docs/ack/tasks.yaml`、
|
|
||||||
`docs/ack/knowledge.yaml`、可选的 `docs/ack/delivery.yaml` 和可选的
|
4. 检查上述文件是否仍有 `<...>` 占位符。结构校验通过且必填项目事实完整才称「完成」。除非用户明确要求,不提交、不推送。
|
||||||
`docs/ack/regression.yaml`;通用资源始终从当前 ACK Skill 目录读取。
|
|
||||||
- 不把完整 `regression.yaml` 注入上下文;使用 `select_regression.py` 获取有预算的
|
```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` 任务。
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
> 本项目基于 ack v0.19.0。
|
> 本项目基于 ack v0.19.0。
|
||||||
> 通用规范由 `/ack` 从 Skill 自身的 `references/` 读取,本文件只填项目差异。
|
> 通用规范由 `/ack` 从 Skill 自身的 `references/` 读取,本文件只填项目差异。
|
||||||
> 覆盖层文件放在 `docs/ack/project.md`,不占用 `AGENTS.md`。
|
> 覆盖层文件放在 `.pouch/ack/project.md`,不占用 `AGENTS.md`。
|
||||||
> ACK 不会自动修改 `AGENTS.md`、`CLAUDE.md` 或其它 Agent 指令文件。
|
> 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`。
|
> 与 `regression.yaml`。
|
||||||
|
|
||||||
## 项目概览
|
## 项目概览
|
||||||
@@ -13,11 +13,11 @@
|
|||||||
- 技术栈:`TypeScript + React (Vite) + Go`
|
- 技术栈:`TypeScript + React (Vite) + Go`
|
||||||
- 运行命令:`npm run dev`(前端)、`go run ./server`(后端)
|
- 运行命令:`npm run dev`(前端)、`go run ./server`(后端)
|
||||||
- Base URL:`http://localhost:5173`
|
- Base URL:`http://localhost:5173`
|
||||||
- 任务板:`docs/ack/tasks.yaml`
|
- 任务板:`.pouch/ack/tasks.yaml`
|
||||||
- 项目知识:`docs/ack/knowledge.yaml`
|
- 项目知识:`.pouch/ack/knowledge.yaml`
|
||||||
- 交付契约:`docs/ack/delivery.yaml`
|
- 交付契约:`.pouch/ack/delivery.yaml`
|
||||||
- 回归目录:`docs/ack/regression.yaml`
|
- 回归目录:`.pouch/ack/regression.yaml`
|
||||||
- 覆盖层文件:`docs/ack/project.md`
|
- 覆盖层文件:`.pouch/ack/project.md`
|
||||||
|
|
||||||
## 稳定规范(引用,不重复)
|
## 稳定规范(引用,不重复)
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
|
|
||||||
## Worker 路由
|
## Worker 路由
|
||||||
|
|
||||||
结构化配置位于 `docs/ack/tasks.yaml` 的 `project.orchestration`,启动记录位于顶层
|
结构化配置位于 `.pouch/ack/tasks.yaml` 的 `project.orchestration`,启动记录位于顶层
|
||||||
`workerReceipts`。本项目默认使用:
|
`workerReceipts`。本项目默认使用:
|
||||||
|
|
||||||
| 角色 | profile ID | 档位 |
|
| 角色 | profile ID | 档位 |
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ ackVersion: "0.19.0"
|
|||||||
project:
|
project:
|
||||||
name: "notes-web"
|
name: "notes-web"
|
||||||
baseUrl: "http://localhost:5173"
|
baseUrl: "http://localhost:5173"
|
||||||
overlayFile: "docs/ack/project.md"
|
overlayFile: ".pouch/ack/project.md"
|
||||||
knowledgeFile: "docs/ack/knowledge.yaml"
|
knowledgeFile: ".pouch/ack/knowledge.yaml"
|
||||||
deliveryFile: "docs/ack/delivery.yaml"
|
deliveryFile: ".pouch/ack/delivery.yaml"
|
||||||
regressionFile: "docs/ack/regression.yaml"
|
regressionFile: ".pouch/ack/regression.yaml"
|
||||||
orchestration:
|
orchestration:
|
||||||
profileVersion: 1
|
profileVersion: 1
|
||||||
mode: "manual"
|
mode: "manual"
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
## 安装与初始化
|
## 安装与初始化
|
||||||
|
|
||||||
- [ ] ACK Skill 已全局安装或安装到当前项目。
|
- [ ] ACK Skill 已全局安装或安装到当前项目。
|
||||||
- [ ] 已运行 `skiff init ack --project <project-root>`。
|
- [ ] 已运行 `pouch init ack --project <project-root>`。
|
||||||
- [ ] `docs/ack/` 只包含项目自己的 `project.md`、`tasks.yaml`、`knowledge.yaml`、
|
- [ ] `.pouch/ack/` 只包含项目自己的 `project.md`、`tasks.yaml`、`knowledge.yaml`、
|
||||||
默认关闭的 `delivery.yaml` 与空的 `regression.yaml`。
|
默认关闭的 `delivery.yaml` 与空的 `regression.yaml`。
|
||||||
- [ ] 旧项目缺少 `knowledge.yaml` 时,只补空文件及缺失的
|
- [ ] 旧项目缺少 `knowledge.yaml` 时,只补空文件及缺失的
|
||||||
`project.knowledgeFile` 指针,没有重跑初始化或覆盖其它项目状态。
|
`project.knowledgeFile` 指针,没有重跑初始化或覆盖其它项目状态。
|
||||||
@@ -16,10 +16,10 @@
|
|||||||
- [ ] `project.md` 只保存项目差异,不复制 Skill 的通用规范。
|
- [ ] `project.md` 只保存项目差异,不复制 Skill 的通用规范。
|
||||||
- [ ] `tasks.yaml` 的 `project.overlayFile` 指向实际覆盖层。
|
- [ ] `tasks.yaml` 的 `project.overlayFile` 指向实际覆盖层。
|
||||||
- [ ] `tasks.yaml` 的 `project.knowledgeFile` 固定为
|
- [ ] `tasks.yaml` 的 `project.knowledgeFile` 固定为
|
||||||
`docs/ack/knowledge.yaml`。
|
`.pouch/ack/knowledge.yaml`。
|
||||||
- [ ] 新项目的 `project.deliveryFile` 固定为 `docs/ack/delivery.yaml`,顶层有
|
- [ ] 新项目的 `project.deliveryFile` 固定为 `.pouch/ack/delivery.yaml`,顶层有
|
||||||
`deliveryRuns: []`;旧项目未采用交付能力时可无这两项。
|
`deliveryRuns: []`;旧项目未采用交付能力时可无这两项。
|
||||||
- [ ] 新项目的 `project.regressionFile` 固定为 `docs/ack/regression.yaml`,顶层有
|
- [ ] 新项目的 `project.regressionFile` 固定为 `.pouch/ack/regression.yaml`,顶层有
|
||||||
`regressionRuns: []`;旧项目未采用回归能力时可无这两项。
|
`regressionRuns: []`;旧项目未采用回归能力时可无这两项。
|
||||||
- [ ] 技术栈、运行、构建、单测和集成测试命令均来自项目证据。
|
- [ ] 技术栈、运行、构建、单测和集成测试命令均来自项目证据。
|
||||||
- [ ] Coordinator、Developer、Test 的模型档位和升级规则已明确。
|
- [ ] Coordinator、Developer、Test 的模型档位和升级规则已明确。
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
- [ ] `allowedWorktrees` 已废弃(v0.19 起),新任务板不配置;worker 默认在
|
- [ ] `allowedWorktrees` 已废弃(v0.19 起),新任务板不配置;worker 默认在
|
||||||
`--project-root` 工作,其它 worktree 由 launcher 按同 git 仓库且已注册约束放行。
|
`--project-root` 工作,其它 worktree 由 launcher 按同 git 仓库且已注册约束放行。
|
||||||
- [ ] `tasks.yaml` 不需要保存 `repoPath` 或 `devWorktree`;项目状态从当前
|
- [ ] `tasks.yaml` 不需要保存 `repoPath` 或 `devWorktree`;项目状态从当前
|
||||||
`--project-root/docs/ack/` 推导,worker 路径由 `--worktree` 显式指定。
|
`--project-root/.pouch/ack/` 推导,worker 路径由 `--worktree` 显式指定。
|
||||||
|
|
||||||
## 路径权限
|
## 路径权限
|
||||||
|
|
||||||
@@ -121,6 +121,6 @@
|
|||||||
blocked/failed/leftover、未完成任务或未解决环境事件的终端保留且不设 TTL。
|
blocked/failed/leftover、未完成任务或未解决环境事件的终端保留且不设 TTL。
|
||||||
|
|
||||||
首次接入建议选择一个低风险问题跑完整闭环。项目差异写回
|
首次接入建议选择一个低风险问题跑完整闭环。项目差异写回
|
||||||
`docs/ack/project.md`;通用问题回流到 ACK Skill 的 `references/`、`templates/`
|
`.pouch/ack/project.md`;通用问题回流到 ACK Skill 的 `references/`、`templates/`
|
||||||
或 `scripts/`,并更新 `VERSION`。项目特有、跨任务复用且已经验证的经验才写入
|
或 `scripts/`,并更新 `VERSION`。项目特有、跨任务复用且已经验证的经验才写入
|
||||||
`docs/ack/knowledge.yaml`。
|
`.pouch/ack/knowledge.yaml`。
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ Coordinator 发现或读取 open 任务
|
|||||||
直到 Developer 的 worker_done / escalation(含 knowledgeApplied / knowledgeCandidates)
|
直到 Developer 的 worker_done / escalation(含 knowledgeApplied / knowledgeCandidates)
|
||||||
-> writeback fixed_by_dev
|
-> writeback fixed_by_dev
|
||||||
-> 若 delivery.yaml intents.testEnvironment 已启用:Coordinator 先按 deployer
|
-> 若 delivery.yaml intents.testEnvironment 已启用:Coordinator 先按 deployer
|
||||||
绑定拉起 `.skiff/deployer/<env>`,再派 Test;Test 不发明编译或启动命令
|
绑定拉起 `.pouch/deployer/<env>`,再派 Test;Test 不发明编译或启动命令
|
||||||
-> 为 Test 独立解析安全 profile;安全重置同角色空闲 worker,或重新 plan/launch fresh worker
|
-> 为 Test 独立解析安全 profile;安全重置同角色空闲 worker,或重新 plan/launch fresh worker
|
||||||
-> dispatch 给 Test(retesting)
|
-> dispatch 给 Test(retesting)
|
||||||
-> 确认 Test 已开始执行(terminal read 确认任务注入;未开始按环境失败处理)
|
-> 确认 Test 已开始执行(terminal read 确认任务注入;未开始按环境失败处理)
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
本文件定义可选的 `verified -> validation_ready/review_ready/released` 交付阶段。开发、独立复测和
|
本文件定义可选的 `verified -> validation_ready/review_ready/released` 交付阶段。开发、独立复测和
|
||||||
Coordinator 终检仍由 ACK 原有闭环负责;只有选中的任务全部 `verified` 后才能进入
|
Coordinator 终检仍由 ACK 原有闭环负责;只有选中的任务全部 `verified` 后才能进入
|
||||||
交付。项目配置位于 `docs/ack/delivery.yaml`,运行证据写入
|
交付。项目配置位于 `.pouch/ack/delivery.yaml`,运行证据写入
|
||||||
`docs/ack/tasks.yaml.deliveryRuns`。
|
`.pouch/ack/tasks.yaml.deliveryRuns`。
|
||||||
|
|
||||||
## 1. 配置与授权不是一回事
|
## 1. 配置与授权不是一回事
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ channel、environment 或 source revision 漂移时重新确认。
|
|||||||
|
|
||||||
## 3.1 测试环境与发版写在同一份契约
|
## 3.1 测试环境与发版写在同一份契约
|
||||||
|
|
||||||
`docs/ack/delivery.yaml` 是测试环境绑定和版本发布的唯一文档。不要另写操作手册,
|
`.pouch/ack/delivery.yaml` 是测试环境绑定和版本发布的唯一文档。不要另写操作手册,
|
||||||
也不要把其中一项写进 `project.md`。用户用自然语言说明「怎么布测试环境」或
|
也不要把其中一项写进 `project.md`。用户用自然语言说明「怎么布测试环境」或
|
||||||
「怎么发版」时,Coordinator 把两者都维护进这份文件的 `intents`。
|
「怎么发版」时,Coordinator 把两者都维护进这份文件的 `intents`。
|
||||||
|
|
||||||
@@ -62,17 +62,18 @@ channel、environment 或 source revision 漂移时重新确认。
|
|||||||
intents:
|
intents:
|
||||||
testEnvironment:
|
testEnvironment:
|
||||||
via: deployer
|
via: deployer
|
||||||
env: test # 项目 .skiff/deployer/test;尚未说明时为 null
|
env: test # 项目 .pouch/deployer/test;尚未说明时为 null
|
||||||
release: null # profile ID,或 null
|
release: null # profile ID,或 null
|
||||||
```
|
```
|
||||||
|
|
||||||
- `testEnvironment` 绑定 deployer skill 的项目环境目录。用户说「重新布测试环境」
|
- `testEnvironment` 绑定 deployer skill 的项目环境目录。用户说「重新布测试环境」
|
||||||
「我要测试」时,ACK 加载 deployer 的 `SKILL.md`,对 `.skiff/deployer/<env>`
|
「我要测试」时,ACK 加载 deployer 的 `SKILL.md`,对 `.pouch/deployer/<env>`
|
||||||
按服务执行 sync + up 和健康检查。派发 Test 复测或跑回归前,若该 intent 已配置
|
按服务执行 sync + up 和健康检查。环境目录不存在或 deployer `check.py` 未通过
|
||||||
且 `enabled: true`,Coordinator 也先执行它。不要求当前有 `verified` 任务。
|
时,加载 deployer skill 的「初始化」,不要在 ACK 里复制 compose 命令。派发
|
||||||
Test 不对这个 intent 发明编译或启动命令。旧的 profile ID 字符串不再执行,必须
|
Test 复测或跑回归前,若该 intent 已配置且 `enabled: true`,Coordinator 也先
|
||||||
迁到 `{via: deployer, env: <env>}`。本地进程启动写在 `project.md`,不算这个
|
执行它。不要求当前有 `verified` 任务。Test 不对这个 intent 发明编译或启动
|
||||||
intent。
|
命令。旧的 profile ID 字符串不再执行,必须迁到 `{via: deployer, env: <env>}`。
|
||||||
|
本地进程启动写在 `project.md`,不算这个 intent。
|
||||||
- `release` 指向 `stopAt: released` 的 profile。用户说「发布一个版本」时执行它。
|
- `release` 指向 `stopAt: released` 的 profile。用户说「发布一个版本」时执行它。
|
||||||
口头「发版」不能代替 stable/production 的 `approval` 步骤。
|
口头「发版」不能代替 stable/production 的 `approval` 步骤。
|
||||||
- 对应 intent 为 `null` 或交付未启用:停止,请用户说明怎么做,按「交付配置维护」
|
- 对应 intent 为 `null` 或交付未启用:停止,请用户说明怎么做,按「交付配置维护」
|
||||||
@@ -90,7 +91,7 @@ intents:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 <ack-skill-dir>/scripts/validate_delivery.py \
|
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>
|
--project-root <project-root>
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -111,7 +112,7 @@ intents:
|
|||||||
- `verify`:运行指定 entrypoint,失败即停止。
|
- `verify`:运行指定 entrypoint,失败即停止。
|
||||||
- `pull-request`:在精确 source revision 上提交、推送任务分支并创建或复用 Draft
|
- `pull-request`:在精确 source revision 上提交、推送任务分支并创建或复用 Draft
|
||||||
PR/MR。普通任务使用项目已确认的 Forge 流程;只有本次是版本发布生命周期且用户
|
PR/MR。普通任务使用项目已确认的 Forge 流程;只有本次是版本发布生命周期且用户
|
||||||
明确要求时才调用独立的 `manage-release`。没有对应能力或认证时标记 blocked,不用
|
明确要求时才调用独立的 `releaser`。没有对应能力或认证时标记 blocked,不用
|
||||||
带 token 的临时 curl 兜底。remote 与 base branch 必须来自该步骤,不能临时猜测。
|
带 token 的临时 curl 兜底。remote 与 base branch 必须来自该步骤,不能临时猜测。
|
||||||
- `build`:调用 artifact 的 build entrypoint。DEB 必须记录包名、版本、架构和
|
- `build`:调用 artifact 的 build entrypoint。DEB 必须记录包名、版本、架构和
|
||||||
SHA-256;OCI image 必须记录完整引用、platform 和 digest。产物必须绑定当前 source
|
SHA-256;OCI image 必须记录完整引用、platform 和 digest。产物必须绑定当前 source
|
||||||
@@ -152,9 +153,9 @@ ID 指向不同 commit、digest 或目标时停止,不覆盖或另建伪装成
|
|||||||
## 7. 与低层 Skill 的边界
|
## 7. 与低层 Skill 的边界
|
||||||
|
|
||||||
ACK 只负责读取项目交付契约、编排顺序、守住审批点并汇总证据,不复制低层 skill 的
|
ACK 只负责读取项目交付契约、编排顺序、守住审批点并汇总证据,不复制低层 skill 的
|
||||||
上传、镜像、Git 发布或远程 Compose 实现。`builder`、`manage-release` 和
|
上传、镜像、Git 发布或远程 Compose 实现。`builder`、`releaser` 和
|
||||||
`deployer` 仍是可独立使用、独立安装的能力。运行测试环境时 ACK 必须加载
|
`deployer` 仍是可独立使用、独立安装的能力。运行测试环境时 ACK 必须加载
|
||||||
deployer skill,不能把 compose/rsync/远程 docker 命令写进 ACK。发版步骤缺失
|
deployer skill,不能把 compose/rsync/远程 docker 命令写进 ACK。发版步骤缺失
|
||||||
builder 或 manage-release 时,ACK 使用契约中已审查的项目 entrypoint,二者都不可用
|
builder 或 releaser 时,ACK 使用契约中已审查的项目 entrypoint,二者都不可用
|
||||||
时把对应步骤标为 `blocked`。低层 skill 自身要求显式调用时,ACK 不能绕过它的
|
时把对应步骤标为 `blocked`。低层 skill 自身要求显式调用时,ACK 不能绕过它的
|
||||||
触发与授权边界;ACK 内部调用 deployer 布测试环境是该 skill 的合法调用路径。
|
触发与授权边界;ACK 内部调用 deployer 布测试环境是该 skill 的合法调用路径。
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
## 项目配置与 Base 结构
|
## 项目配置与 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` 和字段映射:
|
`workflow: clarified-writeback-v1`、显式 `profile`、`baseToken`、`tableId`、`viewId` 和字段映射:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@@ -34,8 +34,8 @@ fields:
|
|||||||
调整当前 view 的可见字段,不删除旧列:
|
调整当前 view 的可见字段,不删除旧列:
|
||||||
|
|
||||||
```bash
|
```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-plan .pouch/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-apply .pouch/ack/tasks.yaml \
|
||||||
--expected-schema-fingerprint <schemaFingerprint>
|
--expected-schema-fingerprint <schemaFingerprint>
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -68,11 +68,11 @@ locale;调用者环境中的凭据和运行时注入变量不会传入。不
|
|||||||
## 读取与整理
|
## 读取与整理
|
||||||
|
|
||||||
```bash
|
```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)
|
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"
|
--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"
|
--output-dir "$tmpdir"
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -101,7 +101,7 @@ Coordinator 对每条 Bug:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 <ack-skill-dir>/scripts/feishu_bug_intake.py write-draft \
|
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-source-ref <sourceRef> \
|
||||||
--expected-draft-revision <draftRevision> --input <draft.json>
|
--expected-draft-revision <draftRevision> --input <draft.json>
|
||||||
```
|
```
|
||||||
@@ -120,7 +120,7 @@ python3 <ack-skill-dir>/scripts/feishu_bug_intake.py write-draft \
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 <ack-skill-dir>/scripts/feishu_bug_intake.py import-approved \
|
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-source-ref <approved-sourceRef> \
|
||||||
--expected-draft-revision <approvedDraftRevision>
|
--expected-draft-revision <approvedDraftRevision>
|
||||||
```
|
```
|
||||||
@@ -135,7 +135,7 @@ python3 <ack-skill-dir>/scripts/feishu_bug_intake.py import-approved \
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 <ack-skill-dir>/scripts/feishu_bug_intake.py mark-imported \
|
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-source-ref <approved-sourceRef> \
|
||||||
--expected-draft-revision <approvedDraftRevision>
|
--expected-draft-revision <approvedDraftRevision>
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -9,10 +9,10 @@
|
|||||||
|
|
||||||
1. 目标项目根目录。
|
1. 目标项目根目录。
|
||||||
2. ACK Skill 已全局安装或安装到当前项目。
|
2. ACK Skill 已全局安装或安装到当前项目。
|
||||||
3. `skiff` 命令可用。
|
3. `pouch` 命令可用。
|
||||||
|
|
||||||
不要覆盖已有的 `docs/ack/project.md`、`docs/ack/tasks.yaml`、
|
不要覆盖已有的 `.pouch/ack/project.md`、`.pouch/ack/tasks.yaml`、
|
||||||
`docs/ack/knowledge.yaml`、`docs/ack/delivery.yaml`、`docs/ack/regression.yaml`、
|
`.pouch/ack/knowledge.yaml`、`.pouch/ack/delivery.yaml`、`.pouch/ack/regression.yaml`、
|
||||||
`AGENTS.md` 或其它 Agent
|
`AGENTS.md` 或其它 Agent
|
||||||
指令文件。ACK 不会自动
|
指令文件。ACK 不会自动
|
||||||
修改 `AGENTS.md`、`CLAUDE.md` 或其它 Agent 指令文件。不要把 token、`.env`
|
修改 `AGENTS.md`、`CLAUDE.md` 或其它 Agent 指令文件。不要把 token、`.env`
|
||||||
@@ -23,19 +23,19 @@
|
|||||||
在目标项目执行:
|
在目标项目执行:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
skiff init ack
|
pouch init ack
|
||||||
```
|
```
|
||||||
|
|
||||||
或显式指定项目:
|
或显式指定项目:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
skiff init ack --project <project-root>
|
pouch init ack --project <project-root>
|
||||||
```
|
```
|
||||||
|
|
||||||
命令从 ACK Skill 自带的 `templates/` 生成:
|
命令从 ACK Skill 自带的 `templates/` 生成:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
docs/ack/
|
.pouch/ack/
|
||||||
├── project.md
|
├── project.md
|
||||||
├── tasks.yaml
|
├── tasks.yaml
|
||||||
├── knowledge.yaml
|
├── knowledge.yaml
|
||||||
@@ -49,31 +49,31 @@ docs/ack/
|
|||||||
### 旧项目补充知识库
|
### 旧项目补充知识库
|
||||||
|
|
||||||
旧项目已经有 `project.md` 和 `tasks.yaml`、但没有 `knowledge.yaml` 时,不要重跑
|
旧项目已经有 `project.md` 和 `tasks.yaml`、但没有 `knowledge.yaml` 时,不要重跑
|
||||||
`skiff init ack`。先检查现有文件并向用户报告缺失项;用户授权后,只从
|
`pouch init ack`。先检查现有文件并向用户报告缺失项;用户授权后,只从
|
||||||
`templates/knowledge.template.yaml` 生成 `docs/ack/knowledge.yaml`,替换项目名和
|
`templates/knowledge.template.yaml` 生成 `.pouch/ack/knowledge.yaml`,替换项目名和
|
||||||
当前时间,保留 `entries: []`。如果现有任务板缺少
|
当前时间,保留 `entries: []`。如果现有任务板缺少
|
||||||
`project.knowledgeFile`,同一次授权只补
|
`project.knowledgeFile`,同一次授权只补
|
||||||
`knowledgeFile: docs/ack/knowledge.yaml`,不改写其它项目状态。生成后运行任务板、
|
`knowledgeFile: .pouch/ack/knowledge.yaml`,不改写其它项目状态。生成后运行任务板、
|
||||||
知识库和跨文件引用校验。
|
知识库和跨文件引用校验。
|
||||||
|
|
||||||
### 旧项目补充交付配置
|
### 旧项目补充交付配置
|
||||||
|
|
||||||
`delivery.yaml` 对旧项目是可选能力;缺少它不会影响三角色开发与验证闭环。只有用户
|
`delivery.yaml` 对旧项目是可选能力;缺少它不会影响三角色开发与验证闭环。只有用户
|
||||||
明确要求配置项目交付时,才从 `templates/delivery.template.yaml` 生成文件,同时在
|
明确要求配置项目交付时,才从 `templates/delivery.template.yaml` 生成文件,同时在
|
||||||
任务板补 `project.deliveryFile: docs/ack/delivery.yaml` 与顶层
|
任务板补 `project.deliveryFile: .pouch/ack/delivery.yaml` 与顶层
|
||||||
`deliveryRuns: []`。首次生成保持 `enabled: false`,按 `delivery.md` 展示并确认
|
`deliveryRuns: []`。首次生成保持 `enabled: false`,按 `delivery.md` 展示并确认
|
||||||
解析结果后才启用。不要重跑 `skiff init ack`,也不要改写已有任务或知识。
|
解析结果后才启用。不要重跑 `pouch init ack`,也不要改写已有任务或知识。
|
||||||
|
|
||||||
### 旧项目补充回归目录
|
### 旧项目补充回归目录
|
||||||
|
|
||||||
`regression.yaml` 对旧项目是可选能力。用户明确要求回归或授权补齐时,从
|
`regression.yaml` 对旧项目是可选能力。用户明确要求回归或授权补齐时,从
|
||||||
`templates/regression.template.yaml` 生成 `docs/ack/regression.yaml`,并只补
|
`templates/regression.template.yaml` 生成 `.pouch/ack/regression.yaml`,并只补
|
||||||
`project.regressionFile: docs/ack/regression.yaml` 与顶层 `regressionRuns: []`。
|
`project.regressionFile: .pouch/ack/regression.yaml` 与顶层 `regressionRuns: []`。
|
||||||
保持 `cases: []`,不要从聊天虚构用例。
|
保持 `cases: []`,不要从聊天虚构用例。
|
||||||
|
|
||||||
## 完善项目覆盖层
|
## 完善项目覆盖层
|
||||||
|
|
||||||
编辑 `docs/ack/project.md`,填入:
|
编辑 `.pouch/ack/project.md`,填入:
|
||||||
|
|
||||||
- 项目名、技术栈、运行命令和 Base URL。
|
- 项目名、技术栈、运行命令和 Base URL。
|
||||||
- Coordinator、Developer、Test 的实际模型档位。
|
- 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` 起
|
- `ackVersion` 使用 ACK Skill 的合法 SemVer `VERSION`;从 `0.10.0` 起
|
||||||
`project.orchestration` 与顶层 `workerReceipts` 必须同时存在。
|
`project.orchestration` 与顶层 `workerReceipts` 必须同时存在。
|
||||||
@@ -93,12 +93,12 @@ docs/ack/
|
|||||||
自动补交付配置。
|
自动补交付配置。
|
||||||
- `updatedAt` 使用当前带时区时间。
|
- `updatedAt` 使用当前带时区时间。
|
||||||
- `project.name` 使用真实值;`overlayFile` 和 `knowledgeFile` 使用项目内相对路径。
|
- `project.name` 使用真实值;`overlayFile` 和 `knowledgeFile` 使用项目内相对路径。
|
||||||
ACK 从命令行 `--project-root` 下固定的 `docs/ack/` 布局解析项目状态,不把
|
ACK 从命令行 `--project-root` 下固定的 `.pouch/ack/` 布局解析项目状态,不把
|
||||||
`repoPath` 或 `devWorktree` 绝对路径写入任务板。旧任务板中的这两个字段仅兼容读取,
|
`repoPath` 或 `devWorktree` 绝对路径写入任务板。旧任务板中的这两个字段仅兼容读取,
|
||||||
不再参与路径绑定。
|
不再参与路径绑定。
|
||||||
- 新项目的 `project.deliveryFile` 固定为 `docs/ack/delivery.yaml`,并保留顶层
|
- 新项目的 `project.deliveryFile` 固定为 `.pouch/ack/delivery.yaml`,并保留顶层
|
||||||
`deliveryRuns: []`。旧项目只有在采用交付能力时才补这两个字段。
|
`deliveryRuns: []`。旧项目只有在采用交付能力时才补这两个字段。
|
||||||
- 新项目的 `project.regressionFile` 固定为 `docs/ack/regression.yaml`,并保留顶层
|
- 新项目的 `project.regressionFile` 固定为 `.pouch/ack/regression.yaml`,并保留顶层
|
||||||
`regressionRuns: []`。旧项目只有在采用回归能力时才补这两个字段。
|
`regressionRuns: []`。旧项目只有在采用回归能力时才补这两个字段。
|
||||||
- `allowedWorktrees` 已废弃(v0.19 起),新任务板不生成该字段;worker 默认在
|
- `allowedWorktrees` 已废弃(v0.19 起),新任务板不生成该字段;worker 默认在
|
||||||
`--project-root` 工作。模型 allowlist、profiles 和 defaults 使用项目实际允许值。
|
`--project-root` 工作。模型 allowlist、profiles 和 defaults 使用项目实际允许值。
|
||||||
@@ -111,7 +111,7 @@ docs/ack/
|
|||||||
|
|
||||||
## 初始化项目知识
|
## 初始化项目知识
|
||||||
|
|
||||||
新项目的 `docs/ack/knowledge.yaml` 保持 `verificationRegistry: {}` 与
|
新项目的 `.pouch/ack/knowledge.yaml` 保持 `verificationRegistry: {}` 与
|
||||||
`entries: []`。不要从聊天、README、issue 或单次失败中猜测并激活知识。
|
`entries: []`。不要从聊天、README、issue 或单次失败中猜测并激活知识。
|
||||||
|
|
||||||
项目运行 ACK 后,Developer 和 Test 可以通过回报提名 `knowledgeCandidates`;
|
项目运行 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`。
|
`intents.testEnvironment: null` 与 `intents.release: null`。
|
||||||
不要根据 README 或 CI 自动推断并启用发布/部署。用户说明测试环境后,Coordinator
|
不要根据 README 或 CI 自动推断并启用发布/部署。用户说明测试环境后,Coordinator
|
||||||
按 deployer skill 准备 `.skiff/deployer/<env>`,并把
|
把 `intents.testEnvironment` 写成 `{via: deployer, env: <env>}`,若
|
||||||
`intents.testEnvironment` 写成 `{via: deployer, env: <env>}`;发版仍指向 profile。
|
`.pouch/deployer/<env>` 尚未就绪则加载 deployer skill 的「初始化」;发版仍指向
|
||||||
|
profile。ACK 初始化不自动跑 deployer 初始化。
|
||||||
配置中不保存 shell、环境变量值或凭据正文;稳定发布和生产部署必须有显式
|
配置中不保存 shell、环境变量值或凭据正文;稳定发布和生产部署必须有显式
|
||||||
approval 步骤。
|
approval 步骤。
|
||||||
|
|
||||||
## 初始化回归目录
|
## 初始化回归目录
|
||||||
|
|
||||||
新项目的 `docs/ack/regression.yaml` 保持 `cases: []`。不要从 README 或聊天猜测
|
新项目的 `.pouch/ack/regression.yaml` 保持 `cases: []`。不要从 README 或聊天猜测
|
||||||
用例。任务 `verified` 后按 `regression.md` 收获。
|
用例。任务 `verified` 后按 `regression.md` 收获。
|
||||||
|
|
||||||
## 校验
|
## 校验
|
||||||
@@ -145,12 +146,12 @@ approval 步骤。
|
|||||||
Agent 从当前 `SKILL.md` 解析 ACK Skill 目录后运行:
|
Agent 从当前 `SKILL.md` 解析 ACK Skill 目录后运行:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 <ack-skill-dir>/scripts/validate_tasks.py docs/ack/tasks.yaml
|
python3 <ack-skill-dir>/scripts/validate_tasks.py .pouch/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_knowledge.py .pouch/ack/knowledge.yaml --tasks .pouch/ack/tasks.yaml
|
||||||
python3 <ack-skill-dir>/scripts/validate_delivery.py docs/ack/delivery.yaml \
|
python3 <ack-skill-dir>/scripts/validate_delivery.py .pouch/ack/delivery.yaml \
|
||||||
--tasks docs/ack/tasks.yaml --project-root <project-root>
|
--tasks .pouch/ack/tasks.yaml --project-root <project-root>
|
||||||
python3 <ack-skill-dir>/scripts/validate_regression.py docs/ack/regression.yaml \
|
python3 <ack-skill-dir>/scripts/validate_regression.py .pouch/ack/regression.yaml \
|
||||||
--tasks docs/ack/tasks.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` 和
|
- `project.md`、`tasks.yaml`、`knowledge.yaml`、`delivery.yaml` 和
|
||||||
`regression.yaml` 没有未替换的 `<...>` 占位符。
|
`regression.yaml` 没有未替换的 `<...>` 占位符。
|
||||||
- `project.overlayFile` 指向真实文件。
|
- `project.overlayFile` 指向真实文件。
|
||||||
- `project.knowledgeFile` 指向 `docs/ack/knowledge.yaml`。
|
- `project.knowledgeFile` 指向 `.pouch/ack/knowledge.yaml`。
|
||||||
- 新项目的 `project.deliveryFile` 指向 `docs/ack/delivery.yaml`;交付默认关闭。
|
- 新项目的 `project.deliveryFile` 指向 `.pouch/ack/delivery.yaml`;交付默认关闭。
|
||||||
- Developer 与 Test 的验证命令可执行。
|
- Developer 与 Test 的验证命令可执行。
|
||||||
- `project.orchestration` 的 profile/allowlist/defaults 通过校验,自动模式只允许
|
- `project.orchestration` 的 profile/allowlist/defaults 通过校验,自动模式只允许
|
||||||
`read-only` 或 `workspace-write`;旧任务板未迁移时保持手动模式。
|
`read-only` 或 `workspace-write`;旧任务板未迁移时保持手动模式。
|
||||||
@@ -171,12 +172,8 @@ python3 <ack-skill-dir>/scripts/validate_regression.py docs/ack/regression.yaml
|
|||||||
|
|
||||||
## 初始化报告
|
## 初始化报告
|
||||||
|
|
||||||
完成后报告:
|
按 `SKILL.md`「初始化」最后一步的格式报告(`## ack 初始化:完成 | 部分完成 | 阻塞`),
|
||||||
|
列出已具备项、待配置项(路径 + 字段 + 示例)、工具链和下一步。
|
||||||
|
|
||||||
- 创建或确认的项目文件(含回归目录)。
|
只有结构校验通过且必填项目事实完整时才称「完成」;否则称「部分完成」或「阻塞」。
|
||||||
- 检测到的技术栈和验证命令。
|
除非用户明确要求,不提交、不推送。
|
||||||
- 任务板、项目知识和交付契约校验结果。
|
|
||||||
- 仍需用户补充的值。
|
|
||||||
|
|
||||||
只有结构校验通过且必填项目事实完整时才称“初始化完成”;否则称“部分完成”,并列出
|
|
||||||
具体阻塞项。除非用户明确要求,不提交、不推送。
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
# 如何开始一个需求(Kickoff)
|
# 如何开始一个需求(Kickoff)
|
||||||
|
|
||||||
从零开一个需求的启动手册。角色/权限见 `roles-and-permissions.md`,闭环见 `closed-loop.md`,模型见 `model-routing.md`。
|
从零开一个需求的启动手册。本文件只覆盖确认前的产品文档、任务拆分与验收信号。
|
||||||
|
用户确认后的三角色闭环、飞书收件、启动 worker、交付和回归由 `SKILL.md` 按条件加载,
|
||||||
|
不要在本文件开头预读其它规范。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -9,7 +11,7 @@
|
|||||||
**你(发起编排的强模型会话)就是 Coordinator (PM) / 产品。** 你负责写文档、拆任务、
|
**你(发起编排的强模型会话)就是 Coordinator (PM) / 产品。** 你负责写文档、拆任务、
|
||||||
编排、终检,**不亲自写代码、不亲自跑测试**。开发和测试是另起的 worker agent;
|
编排、终检,**不亲自写代码、不亲自跑测试**。开发和测试是另起的 worker agent;
|
||||||
具体 CLI、模型、reasoning effort 和执行模式的机器事实源是
|
具体 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
|
```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`,不要把
|
读取有预算的 project、summary 和可工作任务;已知任务时传 `--task-id`,不要把
|
||||||
完整 tasks.yaml 注入上下文。校验 docs/ack/knowledge.yaml 并用
|
完整 tasks.yaml 注入上下文。校验 .pouch/ack/knowledge.yaml 并用
|
||||||
`scripts/select_knowledge.py` 只读取当前任务相关的 active 条目,再读
|
`scripts/select_knowledge.py` 只读取当前任务相关的 active 条目。
|
||||||
references/roles-and-permissions.md、closed-loop.md、optimization-method.md。
|
本 kickoff 只做确认前的产品文档与任务拆分;用户确认后的闭环、飞书、启动
|
||||||
如果 tasks.yaml 声明 project.deliveryFile,再读取 delivery.yaml 与
|
worker、交付和回归按 ACK Skill 的模式路由按条件加载,不要在本步预读其它
|
||||||
references/delivery.md,但不要把配置本身当作执行授权。
|
references。配置本身不是执行授权。
|
||||||
2. 写产品文档到 docs/(PRD / 交互 / 验收),把需求拆成任务,每个任务的验收写成可观测信号(可见文本 / API 结果 / 交互结果)。
|
2. 写产品文档到 docs/(PRD / 交互 / 验收),把需求拆成任务,每个任务的验收写成可观测信号(可见文本 / API 结果 / 交互结果)。
|
||||||
3. 按任务 scope 从 knowledge.yaml 推荐 active 知识,确认后把固定 revision 的
|
3. 按任务 scope 从 knowledge.yaml 推荐 active 知识,确认后把固定 revision 的
|
||||||
knowledgeRefs 写入任务;不要派发 candidate 或全量知识库。
|
knowledgeRefs 写入任务;不要派发 candidate 或全量知识库。
|
||||||
4. 把任务写进 docs/ack/tasks.yaml(只有你写),校验 tasks.yaml 和 knowledge.yaml。
|
4. 把任务写进 .pouch/ack/tasks.yaml(只有你写),校验 tasks.yaml 和 knowledge.yaml。
|
||||||
5. 先把「产品文档 + 任务拆分 + 验收信号 + 适用知识引用」给我确认;若启用了交付,
|
5. 先把「产品文档 + 任务拆分 + 验收信号 + 适用知识引用」给我确认;若启用了交付,
|
||||||
同时列明本次 profile、目标、停止点与审批步骤。不要急着派发或交付。
|
同时列明本次 profile、目标、停止点与审批步骤。不要急着派发或交付。
|
||||||
6. 我确认后,按 ack 闭环循环:先用 `scripts/launch_worker.py` 校验结构化
|
6. 我确认后,按 ack 闭环循环:先用 `scripts/launch_worker.py` 校验结构化
|
||||||
@@ -41,8 +43,8 @@
|
|||||||
dispatch 测试独立复测 → 你读证据终检 → 回写 tasks.yaml;
|
dispatch 测试独立复测 → 你读证据终检 → 回写 tasks.yaml;
|
||||||
每个任务最多三轮有效产品复验,三轮不过记 leftover 并升级我复盘;环境失败单独
|
每个任务最多三轮有效产品复验,三轮不过记 leftover 并升级我复盘;环境失败单独
|
||||||
记录、恢复并告诉我下一步,不占产品复验轮次。
|
记录、恢复并告诉我下一步,不占产品复验轮次。
|
||||||
7. 所选任务都 verified 后,按 regression.md 给出新增/更新/退役/无回归,确认后写入
|
7. 所选任务都 verified 后,按 SKILL.md「运行回归」给出新增/更新/退役/无回归,确认后写入
|
||||||
docs/ack/regression.yaml。只有本次计划包含交付时才按 profile 顺序执行并写
|
.pouch/ack/regression.yaml。只有本次计划包含交付时才按 profile 顺序执行并写
|
||||||
deliveryRuns;启用 delivery 时不能省略 defaultProfile,默认停在 validation_ready
|
deliveryRuns;启用 delivery 时不能省略 defaultProfile,默认停在 validation_ready
|
||||||
或 review_ready,stable/production 步骤再次向我确认。
|
或 review_ready,stable/production 步骤再次向我确认。
|
||||||
```
|
```
|
||||||
@@ -52,15 +54,15 @@
|
|||||||
## 第 1 步:Coordinator 产出(确认前)
|
## 第 1 步:Coordinator 产出(确认前)
|
||||||
|
|
||||||
1. 产品文档 → `docs/PRD-<feature>.md` 等(Coordinator R/W)。
|
1. 产品文档 → `docs/PRD-<feature>.md` 等(Coordinator R/W)。
|
||||||
2. 任务板 → `docs/ack/tasks.yaml`,每条任务带 `expected` + `verification`,验收写成可观测信号(见 `optimization-method.md` §1)。
|
2. 任务板 → `.pouch/ack/tasks.yaml`,每条任务带 `expected` + `verification`,验收写成可观测信号(可见文本 / API 结果 / 交互结果)。
|
||||||
3. 项目知识 → 从 `docs/ack/knowledge.yaml` 按 component、path、dependency、version
|
3. 项目知识 → 从 `.pouch/ack/knowledge.yaml` 按 component、path、dependency、version
|
||||||
和 tag 推荐 `active` 条目,Coordinator 确认后写入固定 revision 的显式
|
和 tag 推荐 `active` 条目,Coordinator 确认后写入固定 revision 的显式
|
||||||
`knowledgeRefs`。candidate 不参与选择。
|
`knowledgeRefs`。candidate 不参与选择。
|
||||||
4. 校验结构与引用:
|
4. 校验结构与引用:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 <ack-skill-dir>/scripts/validate_tasks.py docs/ack/tasks.yaml
|
python3 <ack-skill-dir>/scripts/validate_tasks.py .pouch/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_knowledge.py .pouch/ack/knowledge.yaml --tasks .pouch/ack/tasks.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
`project.orchestration` 是 worker profile 的机器 SSOT;未知字段、非 allowlist 模型、
|
`project.orchestration` 是 worker profile 的机器 SSOT;未知字段、非 allowlist 模型、
|
||||||
@@ -69,8 +71,8 @@ python3 <ack-skill-dir>/scripts/validate_knowledge.py docs/ack/knowledge.yaml --
|
|||||||
读取任务上下文使用:
|
读取任务上下文使用:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
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 docs/ack/tasks.yaml \
|
python3 <ack-skill-dir>/scripts/select_tasks.py .pouch/ack/tasks.yaml \
|
||||||
--task-id <ack-task-id>
|
--task-id <ack-task-id>
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -86,10 +88,9 @@ python3 <ack-skill-dir>/scripts/select_tasks.py docs/ack/tasks.yaml \
|
|||||||
|
|
||||||
## 第 2 步:决定 worktree
|
## 第 2 步:决定 worktree
|
||||||
|
|
||||||
见 `closed-loop.md` §「子任务放哪」:
|
|
||||||
|
|
||||||
- 需求大 / 要并行 / 要保基线分支干净 → 新建隔离 worktree。
|
- 需求大 / 要并行 / 要保基线分支干净 → 新建隔离 worktree。
|
||||||
- 小改动 / 串行修复 → 当前 worktree 起子 agent。
|
- 小改动 / 串行修复 → 当前 worktree 起子 agent。
|
||||||
|
用户确认后的闭环步骤由 SKILL.md「工作」加载,不要在本步预读其它规范。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -99,7 +100,7 @@ python3 <ack-skill-dir>/scripts/select_tasks.py docs/ack/tasks.yaml \
|
|||||||
terminal 不能单独授权复用。复用候选必须属于同一轮 ACK、处于空闲状态,且角色、
|
terminal 不能单独授权复用。复用候选必须属于同一轮 ACK、处于空闲状态,且角色、
|
||||||
profile、worktree 与启动身份仍完全匹配;还必须通过受信后端清理历史消息并取得可核对
|
profile、worktree 与启动身份仍完全匹配;还必须通过受信后端清理历史消息并取得可核对
|
||||||
的新会话身份。当前 Orca 接口缺少该清理证明,所以 Orca 派发仍创建 fresh worker。
|
的新会话身份。当前 Orca 接口缺少该清理证明,所以 Orca 派发仍创建 fresh worker。
|
||||||
原因和边界见 `model-routing.md` §「Receipt、审计与复用边界」。
|
复用边界以 SKILL.md「启动 worker」为准,不要在本步预读其它规范。
|
||||||
|
|
||||||
先查看目标 profile hash,确认本次结构化配置。这个 hash 只用于审计和漂移比较,
|
先查看目标 profile hash,确认本次结构化配置。这个 hash 只用于审计和漂移比较,
|
||||||
不能用于匹配或复用旧 receipt / 既有终端:
|
不能用于匹配或复用旧 receipt / 既有终端:
|
||||||
@@ -146,7 +147,7 @@ worktree 走同一套 `plan` -> 带 expected fingerprint 的 `launch`。在调
|
|||||||
`allowedWorktrees`)。profile
|
`allowedWorktrees`)。profile
|
||||||
只允许 `read-only` 或 `workspace-write`;v0.10 的 full-access 授权通道尚未实现,
|
只允许 `read-only` 或 `workspace-write`;v0.10 的 full-access 授权通道尚未实现,
|
||||||
任何 bypass、YOLO/force 或关闭 sandbox 的请求都必须失败,不能手写命令兜底。
|
任何 bypass、YOLO/force 或关闭 sandbox 的请求都必须失败,不能手写命令兜底。
|
||||||
选型与升级见 `model-routing.md`。
|
选型与升级以 SKILL.md「启动 worker」为准。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -163,7 +164,7 @@ task-create → dispatch 给 DEV → 先确认 DEV 已开始执行(read/probe
|
|||||||
→ 三轮有效产品失败:leftover,升级复盘,继续下一个
|
→ 三轮有效产品失败:leftover,升级复盘,继续下一个
|
||||||
```
|
```
|
||||||
|
|
||||||
具体命令见 `orca-adapter.md`(Orca)或 `closed-loop.md` §「手动模式」(无 Orca);派发文案见 `prompt-templates.md`。
|
Orca / 手动命令与派发文案由 SKILL.md「启动 worker」按条件加载。
|
||||||
|
|
||||||
Coordinator 只内联本轮 `knowledgeRefs` 指向的少量知识,不要求 worker 全量读取
|
Coordinator 只内联本轮 `knowledgeRefs` 指向的少量知识,不要求 worker 全量读取
|
||||||
知识库。知识正文不得作为自由 shell 执行;需要命令时只能引用项目已审查的检查
|
知识库。知识正文不得作为自由 shell 执行;需要命令时只能引用项目已审查的检查
|
||||||
@@ -175,27 +176,15 @@ Coordinator 只内联本轮 `knowledgeRefs` 指向的少量知识,不要求 wo
|
|||||||
|
|
||||||
## 第 5 步:可选交付
|
## 第 5 步:可选交付
|
||||||
|
|
||||||
用户说「重新布测试环境」时加载 deployer skill 执行
|
用户说「重新布测试环境」或「发布一个版本」时,转入 SKILL.md 对应模式,不要在本步预读交付规范。intent 为 null 时先做交付配置维护。
|
||||||
`intents.testEnvironment` 绑定;「发布一个版本」时按 `delivery.md` §3.1 的
|
|
||||||
release profile 执行。intent 为 null 时先做交付配置维护。
|
|
||||||
|
|
||||||
所选任务 `verified` 后,按 `regression.md` 把本轮黑盒路径收获进
|
所选任务 `verified` 后,转入 SKILL.md「运行回归」给出新增/更新/退役/无回归四选一,用户确认后写入 `.pouch/ack/regression.yaml`。用户说「回归」时先布测试环境,再派 Test 按目录执行。
|
||||||
`docs/ack/regression.yaml`(新增/更新/退役/无回归四选一,用户确认后写入)。
|
|
||||||
用户说「回归」时先布测试环境,再派 Test 按目录执行。
|
|
||||||
|
|
||||||
所选任务都由 Coordinator 标记为 `verified` 后,若用户确认的计划包含交付,按
|
所选任务都由 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`。
|
||||||
`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`。
|
|
||||||
|
|
||||||
## 第 6 步:收尾
|
## 第 6 步:收尾
|
||||||
|
|
||||||
一轮结束时 Coordinator 必须能回答 `optimization-method.md` §「结束条件」的问题:
|
一轮结束时 Coordinator 必须能回答:哪些 verified、哪些 leftover、各失败几轮、工作树是否干净、还有没有未处理项。复验轮次争议以 SKILL.md「边界」为准。
|
||||||
哪些 verified、哪些 leftover、各失败几轮、工作树是否干净、还有没有未处理项。
|
|
||||||
Coordinator 最后标记整轮任务完成后,用 `scripts/reclaim_workers.py` 先 dry-run
|
Coordinator 最后标记整轮任务完成后,用 `scripts/reclaim_workers.py` 先 dry-run
|
||||||
审阅决策、再 `--apply` 关闭所有只关联 `verified` 任务的 Developer/Test 终端并核对
|
审阅决策、再 `--apply` 关闭所有只关联 `verified` 任务的 Developer/Test 终端并核对
|
||||||
回执;receipt 和落盘证据继续保留。仍关联 `blocked`、`failed_retest`、`leftover`
|
回执;receipt 和落盘证据继续保留。仍关联 `blocked`、`failed_retest`、`leftover`
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ profile 升级时不得复用旧 worker,Test 也不得使用 Developer 的强
|
|||||||
|
|
||||||
### 机器事实源
|
### 机器事实源
|
||||||
|
|
||||||
worker 路由的机器可读事实只保存在 `docs/ack/tasks.yaml` 的
|
worker 路由的机器可读事实只保存在 `.pouch/ack/tasks.yaml` 的
|
||||||
`project.orchestration`。`project.md` 可以解释项目为何选某个档位,但不能另写一份
|
`project.orchestration`。`project.md` 可以解释项目为何选某个档位,但不能另写一份
|
||||||
完整启动命令或覆盖机器配置。字段结构以 `templates/tasks.schema.json` 为准。
|
完整启动命令或覆盖机器配置。字段结构以 `templates/tasks.schema.json` 为准。
|
||||||
|
|
||||||
@@ -135,12 +135,12 @@ python3 <ack-skill-dir>/scripts/launch_worker.py launch \
|
|||||||
--expected-launch-fingerprint <plan 中的 sha256:...>
|
--expected-launch-fingerprint <plan 中的 sha256:...>
|
||||||
```
|
```
|
||||||
|
|
||||||
`--project-root` 始终指向保存权威 `docs/ack/tasks.yaml` 的项目根;`--worktree` 是本次
|
`--project-root` 始终指向保存权威 `.pouch/ack/tasks.yaml` 的项目根;`--worktree` 是本次
|
||||||
worker 实际工作的绝对路径,两者可以不同。后者仍必须与项目根属于同一 Git
|
worker 实际工作的绝对路径,两者可以不同。后者仍必须与项目根属于同一 Git
|
||||||
仓库(v0.19 起由 launcher 按 `git worktree list` 注册表 + 同 common-dir 校验,
|
仓库(v0.19 起由 launcher 按 `git worktree list` 注册表 + 同 common-dir 校验,
|
||||||
不再依赖 `allowedWorktrees` 白名单)。
|
不再依赖 `allowedWorktrees` 白名单)。
|
||||||
|
|
||||||
项目状态文件固定从 `--project-root/docs/ack/` 解析;任务板不需要保存 `repoPath` 或
|
项目状态文件固定从 `--project-root/.pouch/ack/` 解析;任务板不需要保存 `repoPath` 或
|
||||||
`devWorktree`。旧任务板中的这两个字段仅作兼容信息,launcher 不使用它们授权或定位。
|
`devWorktree`。旧任务板中的这两个字段仅作兼容信息,launcher 不使用它们授权或定位。
|
||||||
`projectRoot`、任务板内容摘要和 worker worktree identity 都会进入 launch fingerprint,
|
`projectRoot`、任务板内容摘要和 worker worktree identity 都会进入 launch fingerprint,
|
||||||
因此切换权威项目根、任务板内容或 worker 路径后必须重新生成并审阅 plan。
|
因此切换权威项目根、任务板内容或 worker 路径后必须重新生成并审阅 plan。
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ one dispatch = one bug = one acceptance path
|
|||||||
## 8. 优先让测试可执行化
|
## 8. 优先让测试可执行化
|
||||||
|
|
||||||
如果某个问题需要多轮修复,说明它值得沉淀成自动化检查。黑盒回归目录是
|
如果某个问题需要多轮修复,说明它值得沉淀成自动化检查。黑盒回归目录是
|
||||||
`docs/ack/regression.yaml`(见 `regression.md`);可执行脚本仍由 Test 维护在
|
`.pouch/ack/regression.yaml`(见 `regression.md`);可执行脚本仍由 Test 维护在
|
||||||
`<integration_test_paths>`。优先级:
|
`<integration_test_paths>`。优先级:
|
||||||
|
|
||||||
1. 把本轮验收信号收获进 `regression.yaml`。
|
1. 把本轮验收信号收获进 `regression.yaml`。
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ Coordinator 用这些模板向 **Developer** 派发修复、向 **Test** 派发
|
|||||||
- 不要写 tasks.yaml,不要标记 verified。
|
- 不要写 tasks.yaml,不要标记 verified。
|
||||||
- 不要写 knowledge.yaml,不要自行扩展或全量读取知识库;candidate 不是已生效规则。
|
- 不要写 knowledge.yaml,不要自行扩展或全量读取知识库;candidate 不是已生效规则。
|
||||||
- 不要把知识正文或 path/args 拼成 shell 命令。只把 verification.ref 交给
|
- 不要把知识正文或 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>`。
|
<verification-ref> --project-root <project-root>`。
|
||||||
- 不要提交或推送,除非用户明确要求。
|
- 不要提交或推送,除非用户明确要求。
|
||||||
- 最小 diff,只改本任务根因,避免无关重构;若必须先重构请停下说明并请示。
|
- 最小 diff,只改本任务根因,避免无关重构;若必须先重构请停下说明并请示。
|
||||||
@@ -137,9 +137,9 @@ Developer 本轮声称(仅供参考,不作数):
|
|||||||
- 若 worker、权限、服务、测试数据、浏览器或工具导致验收无法完成,明确回报
|
- 若 worker、权限、服务、测试数据、浏览器或工具导致验收无法完成,明确回报
|
||||||
`environmentFailure`,不要把“未验证”写成产品 `signals-failed`;若已有独立产品失败
|
`environmentFailure`,不要把“未验证”写成产品 `signals-failed`;若已有独立产品失败
|
||||||
证据,则分别列出产品信号与环境限制。
|
证据,则分别列出产品信号与环境限制。
|
||||||
- 需要时把本轮通过的黑盒路径写成 `regressionCandidates`(见 regression.md),不要直接改 `docs/ack/regression.yaml`。
|
- 需要时把本轮通过的黑盒路径写成 `regressionCandidates`(见 regression.md),不要直接改 `.pouch/ack/regression.yaml`。
|
||||||
- 对每条适用的 `knowledgeRef`,把它的 verification.ref 交给
|
- 对每条适用的 `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`。
|
<verification-ref> --project-root <project-root>`,并回报 `knowledgeChecks`。
|
||||||
对 candidate 使用独立观测验证,不能复述 Developer 的结论作为证据。
|
对 candidate 使用独立观测验证,不能复述 Developer 的结论作为证据。
|
||||||
|
|
||||||
|
|||||||
@@ -8,15 +8,15 @@
|
|||||||
|
|
||||||
## 1. 目录
|
## 1. 目录
|
||||||
|
|
||||||
项目状态是 `docs/ack/regression.yaml`。任务板用
|
项目状态是 `.pouch/ack/regression.yaml`。任务板用
|
||||||
`project.regressionFile: docs/ack/regression.yaml` 声明,运行证据写在
|
`project.regressionFile: .pouch/ack/regression.yaml` 声明,运行证据写在
|
||||||
`tasks.yaml.regressionRuns`。新项目初始化会生成空目录;旧项目没有该文件时,
|
`tasks.yaml.regressionRuns`。新项目初始化会生成空目录;旧项目没有该文件时,
|
||||||
「运行回归」先停下来,用户授权后再从模板补齐。
|
「运行回归」先停下来,用户授权后再从模板补齐。
|
||||||
|
|
||||||
只有 Coordinator 写 `regression.yaml`。Test 在复测报告里提名
|
只有 Coordinator 写 `regression.yaml`。Test 在复测报告里提名
|
||||||
`regressionCandidates`;用户确认后 Coordinator 落盘,并把 case id 写入任务的
|
`regressionCandidates`;用户确认后 Coordinator 落盘,并把 case id 写入任务的
|
||||||
`regressionRefs`。不要把用例散落到第二份文档,也不要让 Test 直接改
|
`regressionRefs`。不要把用例散落到第二份文档,也不要让 Test 直接改
|
||||||
`docs/ack/`。
|
`.pouch/ack/`。
|
||||||
|
|
||||||
`tests/browser/**` 仍可由 Test 维护可执行脚本。用例可用可选 `automationRef`
|
`tests/browser/**` 仍可由 Test 维护可执行脚本。用例可用可选 `automationRef`
|
||||||
指向那些脚本;没有脚本时 Test worker 按目录里的步骤和验收信号执行。
|
指向那些脚本;没有脚本时 Test worker 按目录里的步骤和验收信号执行。
|
||||||
@@ -30,10 +30,10 @@ Playwright。
|
|||||||
读取时用 `scripts/select_regression.py`,不要把完整目录注入上下文:
|
读取时用 `scripts/select_regression.py`,不要把完整目录注入上下文:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
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 docs/ack/regression.yaml \
|
python3 <ack-skill-dir>/scripts/select_regression.py .pouch/ack/regression.yaml \
|
||||||
--suite full
|
--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
|
--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`。
|
上面的表定义了**边界**(谁能碰什么),这一节定义**能力**(每个角色到底该怎么做好自己的事)。每个角色用同一骨架描述:`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):拆解与终检
|
### 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 | 说明 |
|
| 路径 | Coordinator | Test | Developer | 说明 |
|
||||||
|------|:-----------:|:----:|:---------:|------|
|
|------|:-----------:|:----:|:---------:|------|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ from worker_profiles import ( # noqa: E402
|
|||||||
PROTOCOL_VERSION = LAUNCH_PROTOCOL_VERSION
|
PROTOCOL_VERSION = LAUNCH_PROTOCOL_VERSION
|
||||||
RECEIPT_VERSION = 1
|
RECEIPT_VERSION = 1
|
||||||
ENVIRONMENT_POLICY = "per-cli-allowlist-v1"
|
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_CONTROL_OUTPUT = 1024 * 1024
|
||||||
MAX_RECORD_SIZE = 256 * 1024
|
MAX_RECORD_SIZE = 256 * 1024
|
||||||
LAUNCH_TTL_SECONDS = 120
|
LAUNCH_TTL_SECONDS = 120
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ def _validate_knowledge_location(
|
|||||||
knowledge_path: Path,
|
knowledge_path: Path,
|
||||||
project_root: Path,
|
project_root: Path,
|
||||||
) -> str | None:
|
) -> str | None:
|
||||||
expected = project_root / "docs" / "ack" / "knowledge.yaml"
|
expected = project_root / ".pouch" / "ack" / "knowledge.yaml"
|
||||||
lexical = Path(os.path.abspath(knowledge_path.expanduser()))
|
lexical = Path(os.path.abspath(knowledge_path.expanduser()))
|
||||||
if lexical != expected:
|
if lexical != expected:
|
||||||
return (
|
return (
|
||||||
@@ -171,7 +171,7 @@ def load_authoritative_knowledge(
|
|||||||
) -> tuple[dict[str, Any] | None, str | None]:
|
) -> tuple[dict[str, Any] | None, str | None]:
|
||||||
source_fd, open_error = _open_regular_beneath(
|
source_fd, open_error = _open_regular_beneath(
|
||||||
project_root,
|
project_root,
|
||||||
"docs/ack/knowledge.yaml",
|
".pouch/ack/knowledge.yaml",
|
||||||
require_executable=False,
|
require_executable=False,
|
||||||
)
|
)
|
||||||
if open_error is not None or source_fd is None:
|
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("verification_ref", help="verificationRegistry 中的检查 ID")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--project-root",
|
"--project-root",
|
||||||
help="项目根目录;默认从 knowledge.yaml 的 docs/ack 布局或 Git 推断",
|
help="项目根目录;默认从 knowledge.yaml 的 .pouch/ack 布局或 Git 推断",
|
||||||
)
|
)
|
||||||
args = parser.parse_args(argv)
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
默认返回 smoke 套件。本脚本只输出数据,不执行 steps 或 automationRef。
|
默认返回 smoke 套件。本脚本只输出数据,不执行 steps 或 automationRef。
|
||||||
|
|
||||||
用法:
|
用法:
|
||||||
python3 select_regression.py docs/ack/regression.yaml
|
python3 select_regression.py .pouch/ack/regression.yaml
|
||||||
python3 select_regression.py docs/ack/regression.yaml --suite full
|
python3 select_regression.py .pouch/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 --case-id REG-login-001
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -65,7 +65,7 @@ def select_cases(
|
|||||||
def main(argv: list[str] | None = None) -> int:
|
def main(argv: list[str] | None = None) -> int:
|
||||||
parser = argparse.ArgumentParser(description="选择 ACK 回归用例")
|
parser = argparse.ArgumentParser(description="选择 ACK 回归用例")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"regression", nargs="?", default="docs/ack/regression.yaml"
|
"regression", nargs="?", default=".pouch/ack/regression.yaml"
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--suite",
|
"--suite",
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ def _parser() -> argparse.ArgumentParser:
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"tasks",
|
"tasks",
|
||||||
nargs="?",
|
nargs="?",
|
||||||
default="docs/ack/tasks.yaml",
|
default=".pouch/ack/tasks.yaml",
|
||||||
help="任务板路径",
|
help="任务板路径",
|
||||||
)
|
)
|
||||||
parser.add_argument("--task-id", action="append", default=[])
|
parser.add_argument("--task-id", action="append", default=[])
|
||||||
|
|||||||
@@ -5,9 +5,9 @@
|
|||||||
检查引用、步骤顺序、默认 profile 安全边界、敏感信息和仓库内入口路径。
|
检查引用、步骤顺序、默认 profile 安全边界、敏感信息和仓库内入口路径。
|
||||||
|
|
||||||
用法:
|
用法:
|
||||||
python3 validate_delivery.py docs/ack/delivery.yaml
|
python3 validate_delivery.py .pouch/ack/delivery.yaml
|
||||||
python3 validate_delivery.py docs/ack/delivery.yaml \
|
python3 validate_delivery.py .pouch/ack/delivery.yaml \
|
||||||
--tasks docs/ack/tasks.yaml --project-root <project-root>
|
--tasks .pouch/ack/tasks.yaml --project-root <project-root>
|
||||||
|
|
||||||
退出码: 0 通过 / 1 校验失败 / 2 环境或用法错误。
|
退出码: 0 通过 / 1 校验失败 / 2 环境或用法错误。
|
||||||
"""
|
"""
|
||||||
@@ -707,11 +707,13 @@ def _validate_test_environment_intent(
|
|||||||
return
|
return
|
||||||
if project_root is None:
|
if project_root is None:
|
||||||
return
|
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():
|
if not env_dir.is_dir():
|
||||||
errors.append(
|
errors.append(
|
||||||
"intents.testEnvironment.env: 找不到 "
|
"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")
|
project = tasks.get("project")
|
||||||
if not isinstance(project, dict):
|
if not isinstance(project, dict):
|
||||||
return ["tasks.project 必须是对象"]
|
return ["tasks.project 必须是对象"]
|
||||||
if project.get("deliveryFile") != "docs/ack/delivery.yaml":
|
if project.get("deliveryFile") != ".pouch/ack/delivery.yaml":
|
||||||
errors.append("tasks.project.deliveryFile 必须固定为 docs/ack/delivery.yaml")
|
errors.append("tasks.project.deliveryFile 必须固定为 .pouch/ack/delivery.yaml")
|
||||||
delivery_project = delivery.get("project")
|
delivery_project = delivery.get("project")
|
||||||
if (
|
if (
|
||||||
isinstance(delivery_project, dict)
|
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:
|
def main(argv: list[str] | None = None) -> int:
|
||||||
parser = argparse.ArgumentParser(description="校验 ACK 项目交付契约")
|
parser = argparse.ArgumentParser(description="校验 ACK 项目交付契约")
|
||||||
parser.add_argument("delivery", nargs="?", default="docs/ack/delivery.yaml")
|
parser.add_argument("delivery", nargs="?", default=".pouch/ack/delivery.yaml")
|
||||||
parser.add_argument("--tasks", help="关联的 docs/ack/tasks.yaml")
|
parser.add_argument("--tasks", help="关联的 .pouch/ack/tasks.yaml")
|
||||||
parser.add_argument("--project-root", help="项目根目录;提供后检查入口路径")
|
parser.add_argument("--project-root", help="项目根目录;提供后检查入口路径")
|
||||||
parser.add_argument("--schema", help="delivery.schema.json 路径(默认自动探测)")
|
parser.add_argument("--schema", help="delivery.schema.json 路径(默认自动探测)")
|
||||||
args = parser.parse_args(argv)
|
args = parser.parse_args(argv)
|
||||||
|
|||||||
@@ -189,10 +189,10 @@ def _timestamp(value: Any) -> datetime | None:
|
|||||||
|
|
||||||
|
|
||||||
def infer_project_root(document_path: Path) -> Path | 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()
|
lexical = document_path.expanduser().absolute()
|
||||||
parent = lexical.parent
|
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()
|
return parent.parent.parent.resolve()
|
||||||
for candidate in (parent, *parent.parents):
|
for candidate in (parent, *parent.parents):
|
||||||
if (candidate / ".git").exists():
|
if (candidate / ".git").exists():
|
||||||
@@ -245,7 +245,7 @@ def _tasks_project_root(
|
|||||||
return inferred
|
return inferred
|
||||||
|
|
||||||
# Legacy task boards may still declare repoPath. It is only a fallback for
|
# 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")
|
project = tasks_data.get("project")
|
||||||
repo_path = project.get("repoPath") if isinstance(project, dict) else None
|
repo_path = project.get("repoPath") if isinstance(project, dict) else None
|
||||||
if _nonempty(repo_path):
|
if _nonempty(repo_path):
|
||||||
@@ -779,10 +779,10 @@ def _validate_knowledge_file_binding(
|
|||||||
knowledge_file = project.get("knowledgeFile")
|
knowledge_file = project.get("knowledgeFile")
|
||||||
if not _nonempty(knowledge_file):
|
if not _nonempty(knowledge_file):
|
||||||
return ["[tasks] project.knowledgeFile 必填"]
|
return ["[tasks] project.knowledgeFile 必填"]
|
||||||
if knowledge_file != "docs/ack/knowledge.yaml":
|
if knowledge_file != ".pouch/ack/knowledge.yaml":
|
||||||
errors.append(
|
errors.append(
|
||||||
"[tasks] project.knowledgeFile 必须固定为 "
|
"[tasks] project.knowledgeFile 必须固定为 "
|
||||||
"'docs/ack/knowledge.yaml'"
|
"'.pouch/ack/knowledge.yaml'"
|
||||||
)
|
)
|
||||||
relative = Path(knowledge_file)
|
relative = Path(knowledge_file)
|
||||||
segments = knowledge_file.replace("\\", "/").split("/")
|
segments = knowledge_file.replace("\\", "/").split("/")
|
||||||
@@ -812,7 +812,7 @@ def _validate_knowledge_file_binding(
|
|||||||
binding_root = project_root or declared_root
|
binding_root = project_root or declared_root
|
||||||
if binding_root is None or not binding_root.is_dir():
|
if binding_root is None or not binding_root.is_dir():
|
||||||
errors.append(
|
errors.append(
|
||||||
"[tasks] 无法从 docs/ack 布局确定现有项目根目录;"
|
"[tasks] 无法从 .pouch/ack 布局确定现有项目根目录;"
|
||||||
"请传入 --project-root"
|
"请传入 --project-root"
|
||||||
)
|
)
|
||||||
return errors
|
return errors
|
||||||
@@ -1071,7 +1071,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
parser.add_argument("--tasks", help="可选 tasks.yaml,用于跨文件引用校验")
|
parser.add_argument("--tasks", help="可选 tasks.yaml,用于跨文件引用校验")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--project-root",
|
"--project-root",
|
||||||
help="可选项目根目录;默认从 tasks.yaml 的 docs/ack 布局推断",
|
help="可选项目根目录;默认从 tasks.yaml 的 .pouch/ack 布局推断",
|
||||||
)
|
)
|
||||||
args = parser.parse_args(argv)
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,9 @@
|
|||||||
始终检查用例 ID、验收信号和跨文件引用。本脚本只解析数据,不执行 steps。
|
始终检查用例 ID、验收信号和跨文件引用。本脚本只解析数据,不执行 steps。
|
||||||
|
|
||||||
用法:
|
用法:
|
||||||
python3 validate_regression.py docs/ack/regression.yaml
|
python3 validate_regression.py .pouch/ack/regression.yaml
|
||||||
python3 validate_regression.py docs/ack/regression.yaml \
|
python3 validate_regression.py .pouch/ack/regression.yaml \
|
||||||
--tasks docs/ack/tasks.yaml
|
--tasks .pouch/ack/tasks.yaml
|
||||||
|
|
||||||
退出码: 0 通过 / 1 校验失败 / 2 环境或用法错误。
|
退出码: 0 通过 / 1 校验失败 / 2 环境或用法错误。
|
||||||
"""
|
"""
|
||||||
@@ -241,9 +241,9 @@ def validate_tasks_link(
|
|||||||
if not isinstance(project, dict):
|
if not isinstance(project, dict):
|
||||||
return ["[tasks] project 必须是对象"]
|
return ["[tasks] project 必须是对象"]
|
||||||
regression_file = project.get("regressionFile")
|
regression_file = project.get("regressionFile")
|
||||||
if regression_file != "docs/ack/regression.yaml":
|
if regression_file != ".pouch/ack/regression.yaml":
|
||||||
errors.append(
|
errors.append(
|
||||||
"[tasks] project.regressionFile 必须固定为 docs/ack/regression.yaml"
|
"[tasks] project.regressionFile 必须固定为 .pouch/ack/regression.yaml"
|
||||||
)
|
)
|
||||||
if not isinstance(tasks.get("regressionRuns"), list):
|
if not isinstance(tasks.get("regressionRuns"), list):
|
||||||
errors.append("引用 regressionFile 的任务板必须包含 regressionRuns 列表")
|
errors.append("引用 regressionFile 的任务板必须包含 regressionRuns 列表")
|
||||||
@@ -330,9 +330,9 @@ def validate_all(
|
|||||||
def main(argv: list[str] | None = None) -> int:
|
def main(argv: list[str] | None = None) -> int:
|
||||||
parser = argparse.ArgumentParser(description="校验 ACK 项目回归目录")
|
parser = argparse.ArgumentParser(description="校验 ACK 项目回归目录")
|
||||||
parser.add_argument(
|
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 路径(默认自动探测)")
|
parser.add_argument("--schema", help="regression.schema.json 路径(默认自动探测)")
|
||||||
args = parser.parse_args(argv)
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
|
|||||||
@@ -1009,17 +1009,17 @@ def validate_builtin(data: dict) -> list[str]:
|
|||||||
)
|
)
|
||||||
if (
|
if (
|
||||||
"knowledgeFile" in project
|
"knowledgeFile" in project
|
||||||
and project.get("knowledgeFile") != "docs/ack/knowledge.yaml"
|
and project.get("knowledgeFile") != ".pouch/ack/knowledge.yaml"
|
||||||
):
|
):
|
||||||
errors.append(
|
errors.append(
|
||||||
"project.knowledgeFile 必须固定为 docs/ack/knowledge.yaml"
|
"project.knowledgeFile 必须固定为 .pouch/ack/knowledge.yaml"
|
||||||
)
|
)
|
||||||
if (
|
if (
|
||||||
"deliveryFile" in project
|
"deliveryFile" in project
|
||||||
and project.get("deliveryFile") != "docs/ack/delivery.yaml"
|
and project.get("deliveryFile") != ".pouch/ack/delivery.yaml"
|
||||||
):
|
):
|
||||||
errors.append(
|
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):
|
if "deliveryFile" in project and not isinstance(data.get("deliveryRuns"), list):
|
||||||
errors.append("引用 deliveryFile 的任务板必须包含 deliveryRuns 列表")
|
errors.append("引用 deliveryFile 的任务板必须包含 deliveryRuns 列表")
|
||||||
@@ -1027,10 +1027,10 @@ def validate_builtin(data: dict) -> list[str]:
|
|||||||
errors.append("deliveryRuns 存在时 project.deliveryFile 必须存在")
|
errors.append("deliveryRuns 存在时 project.deliveryFile 必须存在")
|
||||||
if (
|
if (
|
||||||
"regressionFile" in project
|
"regressionFile" in project
|
||||||
and project.get("regressionFile") != "docs/ack/regression.yaml"
|
and project.get("regressionFile") != ".pouch/ack/regression.yaml"
|
||||||
):
|
):
|
||||||
errors.append(
|
errors.append(
|
||||||
"project.regressionFile 必须固定为 docs/ack/regression.yaml"
|
"project.regressionFile 必须固定为 .pouch/ack/regression.yaml"
|
||||||
)
|
)
|
||||||
if "regressionFile" in project and not isinstance(
|
if "regressionFile" in project and not isinstance(
|
||||||
data.get("regressionRuns"), list
|
data.get("regressionRuns"), list
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import sys
|
|||||||
|
|
||||||
MIGRATION_MESSAGE = (
|
MIGRATION_MESSAGE = (
|
||||||
"ACK v0.10 已停用自由 worker command 校验器;请在 "
|
"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。"
|
"scripts/launch_worker.py profile-hash|plan|launch。"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
"$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",
|
"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",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"version",
|
"version",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# 复制为 docs/ack/delivery.yaml。默认关闭;由用户明确配置后再启用。
|
# 复制为 .pouch/ack/delivery.yaml。默认关闭;由用户明确配置后再启用。
|
||||||
version: 1
|
version: 1
|
||||||
updatedAt: "<YYYY-MM-DDTHH:mm:ss+TZ>"
|
updatedAt: "<YYYY-MM-DDTHH:mm:ss+TZ>"
|
||||||
project:
|
project:
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
"$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",
|
"title": "ACK project knowledge guardrails",
|
||||||
"description": "docs/ack/knowledge.yaml 的权威结构。知识只描述约束和验证引用,不保存可执行命令。",
|
"description": ".pouch/ack/knowledge.yaml 的权威结构。知识只描述约束和验证引用,不保存可执行命令。",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["version", "updatedAt", "project", "verificationRegistry", "entries"],
|
"required": ["version", "updatedAt", "project", "verificationRegistry", "entries"],
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# 复制为 docs/ack/knowledge.yaml,替换占位符。结构见 templates/knowledge.schema.json。
|
# 复制为 .pouch/ack/knowledge.yaml,替换占位符。结构见 templates/knowledge.schema.json。
|
||||||
# Developer/Test 只能在任务证据中提出 candidate;只有 Coordinator 写入这里。
|
# Developer/Test 只能在任务证据中提出 candidate;只有 Coordinator 写入这里。
|
||||||
version: 1
|
version: 1
|
||||||
updatedAt: "<YYYY-MM-DDTHH:mm:ss+TZ>"
|
updatedAt: "<YYYY-MM-DDTHH:mm:ss+TZ>"
|
||||||
|
|||||||
@@ -3,12 +3,12 @@
|
|||||||
> 本项目基于 ACK Skill v<ack_version>。通用规范由 `/ack` 从 Skill 自身的
|
> 本项目基于 ACK Skill v<ack_version>。通用规范由 `/ack` 从 Skill 自身的
|
||||||
> `references/` 读取;本文件只保存项目差异。
|
> `references/` 读取;本文件只保存项目差异。
|
||||||
>
|
>
|
||||||
> **本文件是「项目覆盖层」,文件名可配置。** 默认放 `docs/ack/project.md`,
|
> **本文件是「项目覆盖层」,文件名可配置。** 默认放 `.pouch/ack/project.md`,
|
||||||
> 不占用 `AGENTS.md`,避免与团队已有的 `AGENTS.md` 约定冲突。
|
> 不占用 `AGENTS.md`,避免与团队已有的 `AGENTS.md` 约定冲突。
|
||||||
> 若希望 Agent 自动加载,可由项目维护者自行在 `AGENTS.md` 中引用本文件;ACK
|
> 若希望 Agent 自动加载,可由项目维护者自行在 `AGENTS.md` 中引用本文件;ACK
|
||||||
> 不会自动修改 `AGENTS.md`、`CLAUDE.md` 或其它 Agent 指令文件。
|
> 不会自动修改 `AGENTS.md`、`CLAUDE.md` 或其它 Agent 指令文件。
|
||||||
> 无论叫什么,都在 `tasks.yaml` 的 `project.overlayFile` 记录实际路径。
|
> 无论叫什么,都在 `tasks.yaml` 的 `project.overlayFile` 记录实际路径。
|
||||||
> `docs/ack/` 只保存本项目的 `project.md`、`tasks.yaml`、`knowledge.yaml`、默认关闭的
|
> `.pouch/ack/` 只保存本项目的 `project.md`、`tasks.yaml`、`knowledge.yaml`、默认关闭的
|
||||||
> `delivery.yaml` 与空的 `regression.yaml`,
|
> `delivery.yaml` 与空的 `regression.yaml`,
|
||||||
> 不复制或链接 Skill。
|
> 不复制或链接 Skill。
|
||||||
|
|
||||||
@@ -18,11 +18,11 @@
|
|||||||
- 技术栈:`<tech_stack>`
|
- 技术栈:`<tech_stack>`
|
||||||
- 运行命令:`<run_command>`
|
- 运行命令:`<run_command>`
|
||||||
- Base URL:`<base_url>`
|
- Base URL:`<base_url>`
|
||||||
- 任务板:`docs/ack/tasks.yaml`
|
- 任务板:`.pouch/ack/tasks.yaml`
|
||||||
- 项目知识:`docs/ack/knowledge.yaml`
|
- 项目知识:`.pouch/ack/knowledge.yaml`
|
||||||
- 交付契约:`docs/ack/delivery.yaml`(默认关闭)
|
- 交付契约:`.pouch/ack/delivery.yaml`(默认关闭)
|
||||||
- 回归目录:`docs/ack/regression.yaml`
|
- 回归目录:`.pouch/ack/regression.yaml`
|
||||||
- 覆盖层文件:`<overlay_file_path>`(默认 `docs/ack/project.md`)
|
- 覆盖层文件:`<overlay_file_path>`(默认 `.pouch/ack/project.md`)
|
||||||
|
|
||||||
## 通用规范(由 ACK Skill 按需读取)
|
## 通用规范(由 ACK Skill 按需读取)
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
## Worker 路由
|
## Worker 路由
|
||||||
|
|
||||||
机器可校验的模型、reasoning effort、权限模式、默认 profile、允许 worktree 和启动
|
机器可校验的模型、reasoning effort、权限模式、默认 profile、允许 worktree 和启动
|
||||||
receipt 全部以 `docs/ack/tasks.yaml` 的 `project.orchestration` 与顶层
|
receipt 全部以 `.pouch/ack/tasks.yaml` 的 `project.orchestration` 与顶层
|
||||||
`workerReceipts` 为准。本文件不保存可执行 worker 命令。
|
`workerReceipts` 为准。本文件不保存可执行 worker 命令。
|
||||||
|
|
||||||
默认 profile:
|
默认 profile:
|
||||||
@@ -126,7 +126,7 @@ Skill 的 `scripts/run_verification.py` 执行,不直接拼接 path/args。检
|
|||||||
- `delivery.yaml` 默认关闭,只描述能力,不自动授权提交、推送、发布或部署。测试环境
|
- `delivery.yaml` 默认关闭,只描述能力,不自动授权提交、推送、发布或部署。测试环境
|
||||||
绑定 deployer,发版写在 `intents.release`;用户明确要求重新部署测试环境或发布
|
绑定 deployer,发版写在 `intents.release`;用户明确要求重新部署测试环境或发布
|
||||||
版本时才执行对应 intent。常规交付仍在任务 `verified` 且本次 profile 得到确认后
|
版本时才执行对应 intent。常规交付仍在任务 `verified` 且本次 profile 得到确认后
|
||||||
运行。回归目录在 `docs/ack/regression.yaml`,由用户明确要求「回归」时运行。
|
运行。回归目录在 `.pouch/ack/regression.yaml`,由用户明确要求「回归」时运行。
|
||||||
- 默认交付 profile 最多到 `validation_ready` 或 `review_ready`;stable 发布或 production 部署必须有
|
- 默认交付 profile 最多到 `validation_ready` 或 `review_ready`;stable 发布或 production 部署必须有
|
||||||
approval 步骤并再次获得明确批准。配置变更只影响下一次 run。
|
approval 步骤并再次获得明确批准。配置变更只影响下一次 run。
|
||||||
- 每个任务最多派发 3 轮,仍不过标记 `leftover` 并继续下一个。
|
- 每个任务最多派发 3 轮,仍不过标记 `leftover` 并继续下一个。
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
"$id": "https://git.yumee.top/laily/skills/skills/ack/templates/regression.schema.json",
|
"$id": "https://git.yumee.top/laily/skills/skills/ack/templates/regression.schema.json",
|
||||||
"title": "ACK project regression catalog",
|
"title": "ACK project regression catalog",
|
||||||
"description": "docs/ack/regression.yaml 的权威结构。用例是给 Test worker 的可观测信号说明书,不是可执行 DSL。",
|
"description": ".pouch/ack/regression.yaml 的权威结构。用例是给 Test worker 的可观测信号说明书,不是可执行 DSL。",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["version", "updatedAt", "project", "cases"],
|
"required": ["version", "updatedAt", "project", "cases"],
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# 复制为 docs/ack/regression.yaml。只有 Coordinator 写入;Test 通过回报提名。
|
# 复制为 .pouch/ack/regression.yaml。只有 Coordinator 写入;Test 通过回报提名。
|
||||||
# 结构见 templates/regression.schema.json。
|
# 结构见 templates/regression.schema.json。
|
||||||
version: 1
|
version: 1
|
||||||
updatedAt: "<YYYY-MM-DDTHH:mm:ss+TZ>"
|
updatedAt: "<YYYY-MM-DDTHH:mm:ss+TZ>"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
"$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",
|
"title": "ACK task board",
|
||||||
"description": "tasks.yaml 的权威结构。跨语言可用;参考校验实现见 scripts/validate_tasks.py。",
|
"description": "tasks.yaml 的权威结构。跨语言可用;参考校验实现见 scripts/validate_tasks.py。",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
"repoPath": {
|
"repoPath": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"deprecated": true,
|
"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": {
|
"baseUrl": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -57,21 +57,21 @@
|
|||||||
},
|
},
|
||||||
"overlayFile": {
|
"overlayFile": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "项目覆盖层文件路径,默认 docs/ack/project.md,可自定义"
|
"description": "项目覆盖层文件路径,默认 .pouch/ack/project.md,可自定义"
|
||||||
},
|
},
|
||||||
"knowledgeFile": {
|
"knowledgeFile": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "docs/ack/knowledge.yaml",
|
"const": ".pouch/ack/knowledge.yaml",
|
||||||
"description": "项目知识护栏库的唯一权威路径"
|
"description": "项目知识护栏库的唯一权威路径"
|
||||||
},
|
},
|
||||||
"deliveryFile": {
|
"deliveryFile": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "docs/ack/delivery.yaml",
|
"const": ".pouch/ack/delivery.yaml",
|
||||||
"description": "可选项目交付契约的唯一权威路径"
|
"description": "可选项目交付契约的唯一权威路径"
|
||||||
},
|
},
|
||||||
"regressionFile": {
|
"regressionFile": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "docs/ack/regression.yaml",
|
"const": ".pouch/ack/regression.yaml",
|
||||||
"description": "可选项目回归目录的唯一权威路径"
|
"description": "可选项目回归目录的唯一权威路径"
|
||||||
},
|
},
|
||||||
"bugIntake": {
|
"bugIntake": {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# 复制为 docs/ack/tasks.yaml,替换占位符。结构见 templates/tasks.schema.json。
|
# 复制为 .pouch/ack/tasks.yaml,替换占位符。结构见 templates/tasks.schema.json。
|
||||||
version: 1
|
version: 1
|
||||||
updatedAt: "<YYYY-MM-DDTHH:mm:ss+TZ>"
|
updatedAt: "<YYYY-MM-DDTHH:mm:ss+TZ>"
|
||||||
source: "Coordinator (PM) Agent"
|
source: "Coordinator (PM) Agent"
|
||||||
@@ -6,10 +6,10 @@ ackVersion: "<接入时的 ack skill 版本>"
|
|||||||
project:
|
project:
|
||||||
name: "<project_name>"
|
name: "<project_name>"
|
||||||
baseUrl: "<base_url>"
|
baseUrl: "<base_url>"
|
||||||
overlayFile: "docs/ack/project.md"
|
overlayFile: ".pouch/ack/project.md"
|
||||||
knowledgeFile: "docs/ack/knowledge.yaml"
|
knowledgeFile: ".pouch/ack/knowledge.yaml"
|
||||||
deliveryFile: "docs/ack/delivery.yaml"
|
deliveryFile: ".pouch/ack/delivery.yaml"
|
||||||
regressionFile: "docs/ack/regression.yaml"
|
regressionFile: ".pouch/ack/regression.yaml"
|
||||||
# 可选:飞书 Base Bug 收件箱。只保存 profile 名和资源 ID,绝不保存 App Secret。
|
# 可选:飞书 Base Bug 收件箱。只保存 profile 名和资源 ID,绝不保存 App Secret。
|
||||||
# bugIntake:
|
# bugIntake:
|
||||||
# provider: "feishu-base"
|
# provider: "feishu-base"
|
||||||
|
|||||||
+171
-14
@@ -1,51 +1,208 @@
|
|||||||
# builder
|
# builder
|
||||||
|
|
||||||
按统一契约完成项目的 DEB 包与 Docker 镜像构建和发布。规范本体见
|
按统一契约完成项目的 DEB 包、Docker 镜像、Android APK / iOS IPA 构建和发布,
|
||||||
|
或把产物拷到目录 / 容器。规范本体见
|
||||||
[references/contract.md](references/contract.md),`scripts/check.py` 是契约的
|
[references/contract.md](references/contract.md),`scripts/check.py` 是契约的
|
||||||
可执行校验器。
|
可执行校验器。
|
||||||
|
|
||||||
## 什么时候使用
|
## 什么时候使用
|
||||||
|
|
||||||
- "帮我构建这个项目的 DEB / Docker 镜像"
|
- "用 builder 初始化这个项目"
|
||||||
- "把 1.2.3 发布到包仓库 / 镜像仓库"
|
- "帮我构建这个项目的 DEB / Docker 镜像 / APK"
|
||||||
- "检查这个项目的 Makefile 是否符合 builder 契约"
|
- "把 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`
|
对新项目说「用 builder 初始化」。Agent 会探测轨道、按
|
||||||
+ 条件 `deb/docker/push*`,变量 `ARCH/VERSION/DIST_DIR/PROJECT_NAME`)。
|
`templates/makefile.builder` 写出项目根 `makefile.builder`,并列出缺的发布配置。
|
||||||
2. 运行 `python3 -I -S <builder>/scripts/check.py .` 直到全部 PASS。
|
不改用户已有的 `Makefile` 或 `.env`。
|
||||||
3. 在项目根 `.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
|
```text
|
||||||
DEB_SERVER_URL=https://deb.example.com
|
DEB_SERVER_URL=https://deb.example.com
|
||||||
DEB_REPOSITORY=main
|
DEB_REPOSITORY=main
|
||||||
DEB_TOKEN=<token> # 只放 .env 或密钥系统,不进 git
|
DEB_TOKEN=<token> # 只放 .env.builder 或密钥系统,不进 git
|
||||||
DOCKER_REGISTRY=registry.example.com
|
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,再加构建日 `YYYYMMDD`、清洗后的分支名、相对距离、短 SHA。
|
||||||
|
这是排序键,不是在选定下一正式 SemVer。测试与正式进**同一个** apt /
|
||||||
|
Docker 仓库,所以 DEB 必须用 `~` 挂在下一 patch 上:`1.4.2` <
|
||||||
|
`1.4.3~20260908…` < `1.4.3`。已装上一正式版的机器可以升到 bugfix 测试包;
|
||||||
|
同号正式版仍盖住测试。
|
||||||
|
|
||||||
|
分支 `feat/login-v2`,相对 `v1.4.2` 第 7 个 commit,SHA `abc1234`,
|
||||||
|
构建日 `20260908`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
规范 / DEB 1.4.3~20260908.feat-login-v2.7+gabc1234
|
||||||
|
文件名 foo_1.4.3~20260908.feat-login-v2.7+gabc1234_amd64.deb
|
||||||
|
Docker registry/ns/foo:1.4.3-20260908.feat-login-v2.7.gabc1234
|
||||||
|
```
|
||||||
|
|
||||||
|
Docker tag 由规范版本映射:`~` → `-`,`+g` → `.g`(Docker 不允许 `~`)。
|
||||||
|
无参 `apt upgrade` 会从上一正式版升到下一 patch 的测试包,但不会盖住同号
|
||||||
|
正式包。生产若不能自动吃到测试包,应拆仓库/组件。构建日取本机当前日期;
|
||||||
|
设了 `SOURCE_DATE_EPOCH` 则用该时刻的 UTC 日期。
|
||||||
|
|
||||||
|
### 分支名清洗
|
||||||
|
|
||||||
|
小写;`/`、`_` 改为 `-`;去掉其它非法字符;压缩连续 `-`;过长截断。
|
||||||
|
detached HEAD 用 `detached`,CI 可注入 `BUILD_BRANCH` / `CI_COMMIT_BRANCH` /
|
||||||
|
`GITHUB_REF_NAME`。脏工作树不把 `-dirty` 写进版本(发布本身会拒绝)。
|
||||||
|
|
||||||
## 使用示例
|
## 使用示例
|
||||||
|
|
||||||
```text
|
```text
|
||||||
用 builder 检查这个项目的 Makefile 是否符合契约。
|
用 builder 初始化这个项目。
|
||||||
|
用 builder 检查这个项目的 makefile.builder 是否符合契约。
|
||||||
用 builder 构建当前版本的 DEB 和镜像,先不要上传。
|
用 builder 构建当前版本的 DEB 和镜像,先不要上传。
|
||||||
用 builder 把 dist/example_1.2.3_amd64.deb 发布到项目已配置的测试仓库。
|
用 builder 把当前 commit 的产物发布到项目已配置的仓库。
|
||||||
用 builder 发布多平台 linux/amd64,linux/arm64 镜像。
|
用 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/upload_deb.sh` | 上传 `.deb` 到 HTTP 包仓库(multipart package/token/repository_name) |
|
||||||
| `scripts/publish_docker.sh` | buildx 构建 + 推送镜像,远端 digest 验证 |
|
| `scripts/publish_docker.sh` | buildx 构建 + 推送镜像,远端 digest 验证 |
|
||||||
| `scripts/verify_deb.sh` | 核对包元数据、内容与 SHA-256 |
|
| `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。
|
脏工作树策略等完整规则见 contract.md。
|
||||||
|
|||||||
+54
-119
@@ -1,133 +1,68 @@
|
|||||||
---
|
---
|
||||||
name: builder
|
name: builder
|
||||||
description: >-
|
description: >-
|
||||||
按统一契约构建并发布项目的 DEB 包与 Docker 镜像:先校验项目 Makefile 是否符合
|
初始化或检查 makefile.builder 契约,再构建或发布 deb/镜像/Android APK/iOS IPA,
|
||||||
builder 契约(check.py),再 make 构建产物,经授权后用 skill 自带脚本上传并验证。
|
或把产物拷到目录/容器。触发词:初始化 builder、检查 makefile.builder、构建/发布
|
||||||
触发词:构建 deb、发布 deb、上传 deb、推送 apt 仓库、打 Debian 包、构建镜像、
|
deb、推送 apt、构建/发布 Docker 镜像、打 APK/IPA、拷到 File Browser、make push。
|
||||||
发布镜像、推送 Docker 镜像、make push、检查 Makefile 是否符合规范。仅分析打包
|
未授权不上传。Docker 仅用户点名镜像时才走。
|
||||||
逻辑或只构建不上传时也可使用;不会在未获授权时执行任何上传。Docker 轨道保持
|
|
||||||
显式触发:用户点名(builder/publish 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 镜像。
|
- 初始化、接入 builder,或还没有 `makefile.builder`:执行「初始化」。
|
||||||
- 用户要求检查项目 Makefile 是否符合 builder 契约。
|
- 检查契约或发布配置:执行「检查」。
|
||||||
- 用户要求梳理或接通项目现有的 DEB/镜像发布流程。
|
- 构建、发布、上传:执行「工作流」。不要静默初始化。
|
||||||
|
- 不适用:本地安装/卸载 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
|
1. 跑 `check.py <project-dir>`;需要工具链与发布键时加 `--ready`;要实构 deb 时加 `--build`。契约 FAIL 或轨道工具缺失:停下,转入「初始化」,不要绕过校验,不要改用户 Makefile。只缺发布键:允许构建,禁止上传。未接契约的存量项目按 contract.md §6 发现已有入口,上传仍用 builder 脚本;成功交付一次后引导迁到 `makefile.builder`。
|
||||||
python3 -I -S <skill-dir>/scripts/check.py <project-dir> # 静态检查
|
2. 仅当用户明确要求发布、上传或提交时才上传。不猜测生产端点;不把 token 写入输出、文件、提交或回复;不用 `set -x` 跑含凭据的命令。可能覆盖同版本时先问。脏工作树默认拒绝发布;用户明确接受时设 `ALLOW_UNCOMMITTED=1` 并注明未提交修改。
|
||||||
python3 -I -S <skill-dir>/scripts/check.py <project-dir> --build # 额外实构 deb 并核对产物
|
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`。
|
||||||
任一 FAIL:停下修复(引导用 create-makefile skill 补齐),不要绕过校验继续发布。
|
6. 发布成功不能只看「curl/push 已执行」。要有退出码、HTTP 成功或远端 digest;索引异步时报告「上传已接受,索引尚待更新」。最终回复给出包名/镜像引用、版本、架构、SHA-256 或 digest、源 commit、工作区状态和未完成项。
|
||||||
完整要求见 [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,未发生上传。
|
|
||||||
- 发布:构建检查通过,服务端接受上传,仓库可见性已验证或准确标记为待更新。
|
|
||||||
|
|||||||
@@ -7,6 +7,18 @@ builder 脚本只做发布,不做项目特定的构建逻辑。
|
|||||||
分工原则:**make 管构建(项目内、确定性),skill 脚本管发布(跨项目 SSOT),
|
分工原则:**make 管构建(项目内、确定性),skill 脚本管发布(跨项目 SSOT),
|
||||||
Agent 只保留授权判断和歧义处理。**
|
Agent 只保留授权判断和歧义处理。**
|
||||||
|
|
||||||
|
契约文件固定为项目根 `makefile.builder`,调用方式:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make -f makefile.builder <target>
|
||||||
|
```
|
||||||
|
|
||||||
|
不要把 builder 目标写进用户的 `Makefile` 或 `makefile`。`check.py` 只读
|
||||||
|
`makefile.builder`。
|
||||||
|
|
||||||
|
发布凭据固定为项目根 `.env.builder`。不要把这些键写进用户的 `.env`。脚本不读取
|
||||||
|
`.env`。
|
||||||
|
|
||||||
## 1. Make 目标
|
## 1. Make 目标
|
||||||
|
|
||||||
### 必备目标(所有项目)
|
### 必备目标(所有项目)
|
||||||
@@ -16,37 +28,97 @@ Agent 只保留授权判断和歧义处理。**
|
|||||||
| `help` | 分组列出全部目标;首屏含当前版本 |
|
| `help` | 分组列出全部目标;首屏含当前版本 |
|
||||||
| `version` | 输出一行版本号,适合脚本消费 |
|
| `version` | 输出一行版本号,适合脚本消费 |
|
||||||
| `clean` | 只删除明确、受限的构建产物目录 |
|
| `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`、禁止多平台** |
|
| `docker` | 有镜像的项目 | 构建本地单平台镜像 `linux/$(ARCH)`;**禁止 `--push`、禁止多平台** |
|
||||||
| `push-deb` | 同时有 DEB 和镜像的项目 | 仅调 builder 的 `upload_deb.sh` 上传 `dist/*.deb` |
|
| `android` | 有 Android APK/AAB 的项目 | 可与 `build` 相同。产出 `$(DIST_DIR)/<name>_<version>_<arch>.apk`(或 `.aab`);只构建不上传。`ARCHES` 是构建机 ISA;fat APK 只留本机一项 |
|
||||||
| `push-docker` | 同时有 DEB 和镜像的项目 | 仅调 builder 的 `publish_docker.sh` |
|
| `ios` | 有 iOS IPA 的项目 | 可与 `build` 相同。产出 `$(DIST_DIR)/<name>_<version>_<arch>.ipa`;只构建不上传 |
|
||||||
| `push` | 单一产物类型时必备;双产物项目为聚合 | 依序调用对应 push-* 或直接调脚本;是发布的唯一 make 入口 |
|
| `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`。
|
有镜像的判据:项目根存在 `Dockerfile`。
|
||||||
2. 双产物项目必须拆 `push-deb`/`push-docker`,`push` 依序聚合两者;单产物项目一个
|
Android 判据:配方产出 `.apk` / `.aab`。iOS 判据:配方产出 `.ipa`。
|
||||||
`push` 即可。
|
拷贝发布判据:配方调用 `copy_artifact.sh`。
|
||||||
|
2. 多于一种发布轨道必须拆 `push-deb` / `push-docker` / `push-copy`,`push` 依序聚合;
|
||||||
|
单轨道项目一个 `push` 即可。
|
||||||
3. `docker` 目标只能本地构建。多平台镜像无法拆成"make 构建 + 单独推送"
|
3. `docker` 目标只能本地构建。多平台镜像无法拆成"make 构建 + 单独推送"
|
||||||
(`buildx --push` 是一步),因此多平台发布只能走 `publish_docker.sh`。
|
(`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. 变量
|
## 2. 变量
|
||||||
|
|
||||||
| 变量 | 默认 | 说明 |
|
| 变量 | 默认 | 说明 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| `ARCH` | `amd64` | 仅允许 `amd64` \| `arm64`,非法值必须 `$(error)` 报错并提示合法值 |
|
| `ARCHES` | `amd64` | 本项目要发布的架构清单,空格分隔,子集只能是 `amd64` `arm64`。写在 `makefile.builder` 里并提交;不要放进 `.env.builder`。缺省或无 `arches` 目标时视为单架构 `amd64`。不要默认写成双架构 |
|
||||||
| `VERSION` | `` (空) | 为空时由 make 从 `git describe --tags --always --dirty` 推导 |
|
| `ARCH` | `$(firstword $(ARCHES))` | 这一次构建的架构。必须落在 `ARCHES` 里;非法值必须 `$(error)` 并提示合法值。命令行/环境传入时只构建/上传这一项 |
|
||||||
| `DIST_DIR` | `dist` | DEB 产物目录 |
|
| `VERSION` | `` (空) | 为空时按「版本号」节从 Git 祖先稳定 tag 推导;禁止 `sort -V` 取全局最新 |
|
||||||
|
| `DIST_DIR` | `dist` | 产物目录(deb / apk / ipa) |
|
||||||
| `PROJECT_NAME` | git 仓库名 | 包名/镜像名主体 |
|
| `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>`:构建日 `YYYYMMDD`。已设 `SOURCE_DATE_EPOCH` 时用该时刻的 UTC
|
||||||
|
日期,否则用本机当前日期。正式 `X.Y.Z` 不加日期。
|
||||||
|
`<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. 发布环境变量
|
## 3. 发布环境变量
|
||||||
|
|
||||||
### DEB 轨道
|
### DEB 轨道
|
||||||
@@ -58,18 +130,29 @@ Agent 只保留授权判断和歧义处理。**
|
|||||||
| `DEB_REPOSITORY` | 是 | 目标仓库名 |
|
| `DEB_REPOSITORY` | 是 | 目标仓库名 |
|
||||||
| `DEB_UPLOAD_PATH` | 否 | 覆盖默认上传路径 `/api/v2/upload/package` |
|
| `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 轨道
|
||||||
|
|
||||||
| 变量 | 必填 | 说明 |
|
| 变量 | 必填 | 说明 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| `DOCKER_REGISTRY` | 是 | registry 主机,无 scheme |
|
| `DOCKER_REGISTRY` | 是 | registry 主机,无 scheme |
|
||||||
| `DOCKER_REPOSITORY` | 否 | 默认取 git 仓库名 |
|
| `DOCKER_REPOSITORY` | 否 | 默认取 git 仓库名 |
|
||||||
| `IMAGE_TAG` | 否 | 默认 `git describe --tags --always --dirty` |
|
| `IMAGE_TAG` | 否 | 默认由规范版本渲染:正式为 `X.Y.Z`;测试将 `~` 换成 `-`、`+g` 换成 `.g` |
|
||||||
| `PLATFORMS` | 否 | 默认 `linux/amd64`;多平台如 `linux/amd64,linux/arm64` |
|
| `PLATFORMS` | 否 | 本机/本轮覆盖。未设置时由 `ARCHES` 映射为 `linux/<arch>,…`;再没有则 `linux/amd64`。不要把项目的架构清单只写在这里(`.env.builder` 不进 git) |
|
||||||
|
|
||||||
配置来源优先级:shell 已显式设置的值 > 项目根 `.env` > 失败并询问用户。
|
`PLATFORMS` 优先级(高 → 低):用户这轮 `--platform` 或已 export 的 `PLATFORMS` → 命令行 `ARCH=` 映射的 `linux/$(ARCH)`(由 `push-docker` 传入)→ `.env.builder` 的 `PLATFORMS` → `makefile.builder` 的 `ARCHES` → `linux/amd64`。
|
||||||
`.env` 由 builder 脚本自动向上查找并加载(不回显任何值);当前 shell 已设置的值
|
|
||||||
优先于 `.env`。
|
配置来源优先级:shell 已显式设置的值 > 项目根 `.env.builder` > 失败并询问用户。
|
||||||
|
`.env.builder` 由 builder 脚本加载(不回显任何值);当前 shell 已设置的值优先。
|
||||||
|
不要读取或改写用户 `.env`。空值视为未配置。不要提交 `.env.builder`。
|
||||||
|
|
||||||
### 工作区安全
|
### 工作区安全
|
||||||
|
|
||||||
@@ -78,23 +161,34 @@ Agent 只保留授权判断和歧义处理。**
|
|||||||
|
|
||||||
## 4. 脚本解析顺序
|
## 4. 脚本解析顺序
|
||||||
|
|
||||||
push 目标定位 builder 脚本时按以下顺序,命中即用,不做静默兜底:
|
push 目标与 `version.sh` 定位 builder 脚本时按以下顺序,命中即用,不做静默兜底:
|
||||||
|
|
||||||
1. `$BUILDER_SKILL_DIR/scripts/`(特殊安装位置)
|
1. `$BUILDER_SKILL_DIR/scripts/`(特殊安装位置)
|
||||||
2. `$HOME/.skills/skills/builder/scripts/`(标准 clone 位)
|
2. `$HOME/.pouch/skills/builder/scripts/`(标准 clone 位)
|
||||||
|
|
||||||
两个位置都不可用时必须失败并提示:设置 `BUILDER_SKILL_DIR`,或把 skills 仓库
|
两个位置都不可用时必须失败并提示:设置 `BUILDER_SKILL_DIR`,或把 pouch 仓库
|
||||||
clone 到 `~/.skills`。
|
clone 到 `~/.pouch`。
|
||||||
|
|
||||||
## 5. 校验
|
## 5. 校验
|
||||||
|
|
||||||
`python3 -I -S <builder-scripts>/check.py <project-dir> [--build]` 对本项目逐条检查
|
`python3 -I -S <builder-scripts>/check.py <project-dir> [--build]` 对本项目逐条检查
|
||||||
上述要求,任一 FAIL 退出码非零,可直接挂 CI。`--build` 额外实构 `make deb` 并核对
|
上述要求,任一 FAIL 退出码非零,可直接挂 CI。`--build` 额外实构
|
||||||
产物元数据(默认只静态检查配方)。校验失败时的修复路径:用 create-makefile skill
|
`make -f makefile.builder deb` 并核对产物元数据(默认只静态检查配方)。`--ready`
|
||||||
补齐或修正 Makefile,不要绕过校验器。
|
额外检查轨道工具链,以及 `.env.builder` / 环境中的发布键名是否存在且非空(不读取、
|
||||||
|
不打印值;缺键只挡住发布)。校验失败时的修复路径:按 `templates/makefile.builder` 补齐或
|
||||||
|
修正 `makefile.builder`,再跑 check.py,不要绕过校验器,不要改用户 Makefile,
|
||||||
|
不要用 create-makefile(版本推导与本契约冲突)。
|
||||||
|
|
||||||
## 6. 存量项目(legacy fallback)
|
## 6. 存量项目(legacy fallback)
|
||||||
|
|
||||||
未接入契约的项目:builder 仍可按发现流程工作——从 `Makefile`、CI 配置、`debian/`
|
未接入契约的项目:builder 仍可按发现流程工作——从用户 `Makefile`、CI 配置、`debian/`
|
||||||
与发布文档中找已有构建/上传入口,优先复用;上传仍使用 builder 脚本。完成一次成功
|
与发布文档中找已有构建/上传入口,优先复用;上传仍使用 builder 脚本。从项目根查找,
|
||||||
交付后应引导用户用 create-makefile 把项目迁移到本契约,之后以 check.py 为准。
|
不预设文件位置:
|
||||||
|
|
||||||
|
```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。
|
||||||
|
|||||||
@@ -7,15 +7,15 @@
|
|||||||
| Registry | 必须显式确定,例如 `registry.example.com` |
|
| Registry | 必须显式确定,例如 `registry.example.com` |
|
||||||
| Repository | 必须包含项目约定的 namespace;缺省取 git 仓库名 |
|
| Repository | 必须包含项目约定的 namespace;缺省取 git 仓库名 |
|
||||||
| Tag | 必须显式确定;优先使用版本号或 Git SHA |
|
| Tag | 必须显式确定;优先使用版本号或 Git SHA |
|
||||||
| Platform | 必须显式确定,例如 `linux/amd64` 或 `linux/amd64,linux/arm64` |
|
| Platform | 用户这轮显式值,否则 `.env.builder` 的 `PLATFORMS`,否则 `makefile.builder` 的 `ARCHES` 映射为 `linux/<arch>,…`,再否则 `linux/amd64` |
|
||||||
| Dockerfile | 默认 `Dockerfile`,不存在或项目另有约定时明确指定 |
|
| Dockerfile | 默认 `Dockerfile`,不存在或项目另有约定时明确指定 |
|
||||||
| Context | 默认当前项目根目录 |
|
| Context | 默认当前项目根目录 |
|
||||||
|
|
||||||
信息来源优先级:
|
信息来源优先级:
|
||||||
|
|
||||||
1. 用户本次请求中明确给出的值。
|
1. 用户本次请求中明确给出的值。
|
||||||
2. 当前项目的 `.env` 与 `AGENTS.md`、发布文档。
|
2. 当前项目的 `.env.builder` 与 `AGENTS.md`、发布文档。不要读用户 `.env`。
|
||||||
3. Makefile、CI 配置或现有构建脚本中一致且无歧义的配置。
|
3. `makefile.builder`、用户 Makefile、CI 配置或现有构建脚本中一致且无歧义的配置。
|
||||||
4. 询问用户。
|
4. 询问用户。
|
||||||
|
|
||||||
不要从其他项目、shell history 或无关的本地配置中猜测发布目标。
|
不要从其他项目、shell history 或无关的本地配置中猜测发布目标。
|
||||||
@@ -34,7 +34,9 @@
|
|||||||
|
|
||||||
## Tag 策略
|
## Tag 策略
|
||||||
|
|
||||||
- release tag(如 `v1.2.3`)默认视为不可变。
|
- 未显式给出 `--tag` / `IMAGE_TAG` 时,使用契约「版本号」渲染出的 tag
|
||||||
- Git SHA tag 应对应当前源 commit。
|
(正式 `1.4.2`,测试 `1.4.3-<date>.feat-login.7.gabc1234`),不要退回
|
||||||
- `latest`、`stable` 等浮动 tag 只有在用户明确要求时才发布。
|
`git describe`,不要询问后另选一套。
|
||||||
- 用户未给 tag 且项目没有唯一明确规则时,必须询问,不要自行选择。
|
- 正式产物 tag 与 Git tag `vX.Y.Z` 对应,但镜像 tag **不含** `v`;默认视为不可变。
|
||||||
|
- `latest`、`stable`、裸分支名等浮动 tag 只有在用户明确要求时才额外发布,
|
||||||
|
不能代替上面那条唯一身份。
|
||||||
|
|||||||
+447
-47
@@ -1,14 +1,18 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Executable form of the builder contract (references/contract.md).
|
"""Executable form of the builder contract (references/contract.md).
|
||||||
|
|
||||||
Checks a project's Makefile against the contract by probing make itself with
|
Checks a project's makefile.builder against the contract by probing make
|
||||||
dry runs (`make -n`) instead of parsing Makefile text: includes, conditionals,
|
itself with dry runs (`make -f makefile.builder -n`) instead of parsing
|
||||||
and variable expansion are resolved by make, so behavior is what gets judged.
|
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:
|
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.
|
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.
|
Change the contract here first, then mirror the change into contract.md.
|
||||||
"""
|
"""
|
||||||
@@ -17,6 +21,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -27,8 +32,8 @@ ARCH_VALUES = ("amd64", "arm64")
|
|||||||
REQUIRED_TARGETS = ("help", "version", "clean", "build")
|
REQUIRED_TARGETS = ("help", "version", "clean", "build")
|
||||||
UPLOAD_TOKENS = (
|
UPLOAD_TOKENS = (
|
||||||
"curl ", "curl\t", "scp ", "rsync ", "aptly ", "reprepro ",
|
"curl ", "curl\t", "scp ", "rsync ", "aptly ", "reprepro ",
|
||||||
"docker push", "buildx build --push", "buildx --push", "upload_deb.sh",
|
"docker push", "docker cp", "buildx build --push", "buildx --push",
|
||||||
"publish_docker.sh",
|
"upload_deb.sh", "publish_docker.sh", "copy_artifact.sh",
|
||||||
)
|
)
|
||||||
SECRET_PATTERNS = (
|
SECRET_PATTERNS = (
|
||||||
re.compile(r"(TOKEN|PASSWORD|SECRET|API_KEY|PASSWD)[A-Z_]*\s*[:?]?=\s*['\"]?[^\s$({\"']+", re.IGNORECASE),
|
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")
|
FLOATING_TAGS = (":latest", ":stable")
|
||||||
DEB_SHAPE = re.compile(r"^[^_\s]+_[^_\s]+_[^_\s]+\.deb$")
|
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"
|
PASS = "PASS"
|
||||||
FAIL = "FAIL"
|
FAIL = "FAIL"
|
||||||
@@ -58,16 +65,22 @@ class Report:
|
|||||||
self.skips += 0 if self.skips else 1
|
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(
|
return subprocess.run(
|
||||||
["make", "-C", str(project), "-n", *args],
|
cmd, capture_output=True, text=True, timeout=timeout, check=False
|
||||||
capture_output=True, text=True, timeout=timeout, check=False,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def has_no_rule(result: subprocess.CompletedProcess[str]) -> bool:
|
def has_no_rule(result: subprocess.CompletedProcess[str]) -> bool:
|
||||||
|
err = result.stderr.lower()
|
||||||
return result.returncode != 0 and (
|
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
|
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]:
|
def check_required_targets(report: Report, project: Path) -> dict[str, bool]:
|
||||||
present: dict[str, bool] = {}
|
present: dict[str, bool] = {}
|
||||||
lines = []
|
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:
|
def check_arch_guard(report: Report, project: Path) -> None:
|
||||||
|
title = "ARCH / ARCHES 守卫与缺省值"
|
||||||
|
problems: list[str] = []
|
||||||
bad = run_make(project, "build", "ARCH=loongarch")
|
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
|
default_ok = run_make(project, "build").returncode == 0
|
||||||
lines = [
|
lines = [
|
||||||
f"invalid ARCH rejected: {'yes' if guard_ok else 'NO'}",
|
f"invalid ARCH rejected: {'yes' if guard_ok else 'NO'}",
|
||||||
f"default ARCH works: {'yes' if default_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 not guard_ok:
|
||||||
if guard_ok and default_ok:
|
problems.append(
|
||||||
report.add(PASS, 2, "ARCH 守卫与缺省值", "\n".join(lines + hint.splitlines()))
|
"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:
|
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:
|
def check_version_output(report: Report, project: Path) -> None:
|
||||||
result = run_make(project, "version")
|
result = run_make(project, "version")
|
||||||
# Dry run: the echoed `@echo <version>` line IS the would-be output.
|
# 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 = parse_dry_echo(result)
|
||||||
out = "\n".join(out_lines).strip()
|
|
||||||
single = len(out.splitlines()) == 1 and out != ""
|
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(
|
report.add(
|
||||||
PASS if single else FAIL,
|
PASS if no_v else FAIL,
|
||||||
3,
|
3,
|
||||||
"version 输出一行非空版本号",
|
"version 输出一行非空规范版本(无 v 前缀)",
|
||||||
f"stdout={out!r}",
|
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}")
|
problems.append(f"dpkg-deb --info failed on {built_deb.name}")
|
||||||
else:
|
else:
|
||||||
problems.append("dpkg-deb unavailable; metadata not verified (--build)")
|
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:
|
if problems:
|
||||||
report.add(FAIL, 5, "deb 目标产物形状与纯构建", "\n".join(problems))
|
report.add(FAIL, 5, "deb 目标产物形状与纯构建", "\n".join(problems))
|
||||||
else:
|
else:
|
||||||
@@ -180,6 +285,45 @@ def detect_docker_project(project: Path) -> bool:
|
|||||||
return (project / "Dockerfile").exists() or (project / "docker-compose.yaml").exists()
|
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:
|
def check_docker_recipe(report: Report, project: Path) -> None:
|
||||||
dry = run_make(project, "docker")
|
dry = run_make(project, "docker")
|
||||||
text = chr(10).join(clean_make_output(dry))
|
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(
|
SCRIPT_RESOLVE_SNIPPETS = tuple(
|
||||||
f"{prefix}{name}"
|
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
|
for name in VALID_SCRIPT_NAMES
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def check_push_delegates(report: Report, project: Path, dual_artifact: bool) -> None:
|
def check_push_delegates(
|
||||||
targets = ("push-deb", "push-docker") if dual_artifact else ("push",)
|
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 = []
|
missing = []
|
||||||
inline = []
|
inline = []
|
||||||
thin = []
|
thin = []
|
||||||
|
builder_names = ("upload_deb.sh", "publish_docker.sh", "copy_artifact.sh")
|
||||||
for target in targets:
|
for target in targets:
|
||||||
dry = run_make(project, target)
|
dry = run_make(project, target)
|
||||||
if has_no_rule(dry):
|
if has_no_rule(dry):
|
||||||
missing.append(target)
|
missing.append(target)
|
||||||
continue
|
continue
|
||||||
text = chr(10).join(clean_make_output(dry))
|
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:
|
if bad_tokens:
|
||||||
inline.append(f"{target}: inline upload command ({', '.join(bad_tokens)})")
|
inline.append(f"{target}: inline upload command ({', '.join(bad_tokens)})")
|
||||||
elif not any(snippet in text for snippet in SCRIPT_RESOLVE_SNIPPETS) \
|
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 "$(BUILDER_SCRIPT)" not in text \
|
||||||
and "publish_docker.sh" 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 ~/.skills/... path)")
|
inline.append(f"{target}: does not call a builder script (expected $BUILDER_SKILL_DIR/... or ~/.pouch/... path)")
|
||||||
else:
|
else:
|
||||||
thin.append(target)
|
thin.append(target)
|
||||||
problems = []
|
problems = []
|
||||||
if missing:
|
if missing:
|
||||||
problems.append("missing targets: " + ", ".join(missing))
|
problems.append("missing targets: " + ", ".join(missing))
|
||||||
problems.extend(inline)
|
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
|
status = PASS if not problems else FAIL
|
||||||
detail = "\n".join(problems) if problems else "thin wrappers: " + ", ".join(thin)
|
detail = "\n".join(problems) if problems else "thin wrappers: " + ", ".join(thin)
|
||||||
report.add(status, 7, "push 仅委托 builder 脚本(薄包装)", detail)
|
report.add(status, 7, "push 仅委托 builder 脚本(薄包装)", detail)
|
||||||
|
|
||||||
|
|
||||||
def check_secrets_and_tags(report: Report, project: Path) -> None:
|
def check_secrets_and_tags(report: Report, project: Path) -> None:
|
||||||
makefile = project / "Makefile"
|
makefile = project / BUILDER_MAKEFILE
|
||||||
included_text = ""
|
|
||||||
problems = []
|
problems = []
|
||||||
files = [makefile]
|
files = [makefile]
|
||||||
if makefile.exists():
|
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")
|
report.add(FAIL if problems else PASS, 8, "无内联机密、无隐式 latest/stable", "\n".join(problems) or "clean")
|
||||||
|
|
||||||
|
|
||||||
def check_script_paths(report: Report) -> None:
|
DEB_ENV_KEYS = ("DEB_SERVER_URL", "DEB_TOKEN", "DEB_REPOSITORY")
|
||||||
import os
|
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 = []
|
candidates = []
|
||||||
env_dir = os.environ.get("BUILDER_SKILL_DIR")
|
env_dir = os.environ.get("BUILDER_SKILL_DIR")
|
||||||
if env_dir:
|
if env_dir:
|
||||||
candidates.append(Path(env_dir) / "scripts")
|
candidates.append(Path(env_dir) / "scripts")
|
||||||
home = Path(os.environ.get("HOME", ""))
|
home = Path(os.environ.get("HOME", ""))
|
||||||
|
candidates.append(home / ".pouch" / "skills" / "builder" / "scripts")
|
||||||
candidates.append(home / ".skills" / "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)
|
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:
|
if found:
|
||||||
@@ -270,31 +574,85 @@ def check_script_paths(report: Report) -> None:
|
|||||||
report.add(FAIL, 9, "builder 脚本路径可达", "\n".join([
|
report.add(FAIL, 9, "builder 脚本路径可达", "\n".join([
|
||||||
"none of these resolve to scripts/upload_deb.sh:",
|
"none of these resolve to scripts/upload_deb.sh:",
|
||||||
*(f" {c}" for c in candidates),
|
*(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:
|
def build_project(project: Path) -> Path | None:
|
||||||
"""Run `make deb` for real and return the produced .deb, or None."""
|
"""Run `make -f makefile.builder 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)
|
result = run_make(project, "deb", timeout=1800, dry_run=False)
|
||||||
if result.returncode != 0:
|
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
|
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)
|
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
|
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:
|
def main(argv: list[str] | None = None) -> int:
|
||||||
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||||
parser.add_argument("project", type=Path, help="project directory containing the Makefile")
|
parser.add_argument(
|
||||||
parser.add_argument("--build", action="store_true", help="actually run `make deb` and verify the artifact")
|
"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)
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
project = args.project.resolve()
|
project = args.project.resolve()
|
||||||
makefile = project / "Makefile"
|
makefile = project / BUILDER_MAKEFILE
|
||||||
if not makefile.is_file():
|
if not makefile.is_file():
|
||||||
print(f"Error: no Makefile in {project}", file=sys.stderr)
|
if not args.ready:
|
||||||
return 2
|
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:
|
if shutil.which("make") is None:
|
||||||
print("Error: make is required.", file=sys.stderr)
|
print("Error: make is required.", file=sys.stderr)
|
||||||
return 2
|
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).
|
# Gather every recipe once via dry-running all known targets (best effort).
|
||||||
recipe_all_parts = []
|
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)
|
result = run_make(project, target)
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
recipe_all_parts.append(result.stdout)
|
recipe_all_parts.append(result.stdout)
|
||||||
@@ -314,19 +682,22 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
built_deb: Path | None = None
|
built_deb: Path | None = None
|
||||||
deb_project = detect_deb_project(recipe_all, project)
|
deb_project = detect_deb_project(recipe_all, project)
|
||||||
docker_project = detect_docker_project(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"]:
|
if present["build"]:
|
||||||
check_arch_guard(report, project)
|
check_arch_guard(report, project)
|
||||||
check_version_output(report, project)
|
check_version_output(report, project)
|
||||||
check_build_has_no_upload(report, project)
|
check_build_has_no_upload(report, project)
|
||||||
else:
|
else:
|
||||||
report.add(SKIP, 2, "ARCH 守卫与缺省值", "(build target missing)")
|
report.add(SKIP, 2, "ARCH / ARCHES 守卫与缺省值", "(build target missing)")
|
||||||
report.add(SKIP, 3, "version 输出一行非空版本号", "(version target missing)")
|
report.add(SKIP, 3, "version 输出一行非空规范版本(无 v 前缀)", "(version target missing)")
|
||||||
report.add(SKIP, 4, "build 不含上传动作", "(build target missing)")
|
report.add(SKIP, 4, "build 不含上传动作", "(build target missing)")
|
||||||
|
|
||||||
if deb_project:
|
if deb_project:
|
||||||
if args.build:
|
if args.build:
|
||||||
print("--build: running `make deb` ...")
|
print(f"--build: running `make -f {BUILDER_MAKEFILE} deb` ...")
|
||||||
built_deb = build_project(project)
|
built_deb = build_project(project)
|
||||||
if built_deb is None:
|
if built_deb is None:
|
||||||
print("--build: no .deb produced; artifact checks degrade to recipe-only.", file=sys.stderr)
|
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:
|
else:
|
||||||
report.add(SKIP, 6, "docker 目标为本地单平台构建", "(no Dockerfile)")
|
report.add(SKIP, 6, "docker 目标为本地单平台构建", "(no Dockerfile)")
|
||||||
|
|
||||||
dual = deb_project and docker_project
|
if android_project:
|
||||||
check_push_delegates(report, project, dual)
|
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_secrets_and_tags(report, project)
|
||||||
check_script_paths(report)
|
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
|
total_fail = report.failures
|
||||||
print()
|
print()
|
||||||
|
|||||||
Executable
+152
@@ -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"
|
||||||
@@ -6,8 +6,8 @@ usage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Build and publish a Docker image with buildx. Configuration comes from the
|
# Build and publish a Docker image with buildx. Configuration comes from the
|
||||||
# environment first (optionally loaded from the project root .env); flags
|
# environment first (optionally loaded from the project root .env.builder);
|
||||||
# override.
|
# flags override. Does not read `.env`.
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# publish_docker.sh [--registry HOST] [--repository PATH] [--tag TAG] \
|
# publish_docker.sh [--registry HOST] [--repository PATH] [--tag TAG] \
|
||||||
@@ -16,8 +16,9 @@ usage() {
|
|||||||
# Environment:
|
# Environment:
|
||||||
# DOCKER_REGISTRY Required (or --registry)
|
# DOCKER_REGISTRY Required (or --registry)
|
||||||
# DOCKER_REPOSITORY Optional, default: git repository name (or --repository)
|
# DOCKER_REPOSITORY Optional, default: git repository name (or --repository)
|
||||||
# IMAGE_TAG Optional, default: git describe --tags --always --dirty (or --tag)
|
# IMAGE_TAG Optional, default: version.sh --docker (or --tag)
|
||||||
# PLATFORMS Optional, default: linux/amd64 (or --platform)
|
# PLATFORMS Optional. Else makefile.builder ARCHES mapped to
|
||||||
|
# linux/<arch>,… ; else linux/amd64 (or --platform)
|
||||||
# DOCKER_DOCKERFILE Optional, default: Dockerfile (--file)
|
# DOCKER_DOCKERFILE Optional, default: Dockerfile (--file)
|
||||||
# DOCKER_CONTEXT Optional, default: . (--context)
|
# DOCKER_CONTEXT Optional, default: . (--context)
|
||||||
# DOCKER_BUILDER Optional buildx builder name (--builder)
|
# DOCKER_BUILDER Optional buildx builder name (--builder)
|
||||||
@@ -30,18 +31,19 @@ usage() {
|
|||||||
|
|
||||||
project_root=$(git rev-parse --show-toplevel 2>/dev/null || true)
|
project_root=$(git rev-parse --show-toplevel 2>/dev/null || true)
|
||||||
|
|
||||||
# Load project .env without printing values; explicitly exported shell values keep precedence.
|
# Load .env.builder without printing values; shell values win. Do not read `.env`.
|
||||||
if [[ -n "$project_root" && -f "$project_root/.env" ]]; then
|
if [[ -n "$project_root" && -f "$project_root/.env.builder" ]]; then
|
||||||
while IFS='=' read -r key value; do
|
while IFS='=' read -r key value; do
|
||||||
key=${key%%[[:space:]]*}
|
key=${key%%[[:space:]]*}
|
||||||
[[ -z "$key" || "$key" == \#* ]] && continue
|
[[ -z "$key" || "$key" == \#* ]] && continue
|
||||||
if [[ -n "${!key:-}" ]]; then
|
if [[ -n "${!key:-}" ]]; then
|
||||||
continue # shell value already set: wins over .env
|
continue # shell value already set: wins over .env.builder
|
||||||
fi
|
fi
|
||||||
value=${value%\"}; value=${value#\"}; value=${value%\'}; value=${value#\'}
|
value=${value%\"}; value=${value#\"}; value=${value%\'}; value=${value#\'}
|
||||||
|
[[ -z "$value" ]] && continue
|
||||||
printf -v "$key" '%s' "$value"
|
printf -v "$key" '%s' "$value"
|
||||||
export "$key"
|
export "$key"
|
||||||
done < <(grep -v '^[[:space:]]*$' "$project_root/.env")
|
done < <(grep -v '^[[:space:]]*$' "$project_root/.env.builder")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
git_repo_name=
|
git_repo_name=
|
||||||
@@ -52,7 +54,7 @@ fi
|
|||||||
registry=${DOCKER_REGISTRY:-}
|
registry=${DOCKER_REGISTRY:-}
|
||||||
repository=${DOCKER_REPOSITORY:-$git_repo_name}
|
repository=${DOCKER_REPOSITORY:-$git_repo_name}
|
||||||
tag=${IMAGE_TAG:-}
|
tag=${IMAGE_TAG:-}
|
||||||
platform=${PLATFORMS:-linux/amd64}
|
platform=${PLATFORMS:-}
|
||||||
dockerfile=${DOCKER_DOCKERFILE:-Dockerfile}
|
dockerfile=${DOCKER_DOCKERFILE:-Dockerfile}
|
||||||
build_context=${DOCKER_CONTEXT:-.}
|
build_context=${DOCKER_CONTEXT:-.}
|
||||||
builder=${DOCKER_BUILDER:-}
|
builder=${DOCKER_BUILDER:-}
|
||||||
@@ -81,7 +83,7 @@ if [[ -n "$registry" && ( "$registry" == *://* || "$registry" == */* ) ]]; then
|
|||||||
fi
|
fi
|
||||||
if [[ -z "$registry" ]]; then
|
if [[ -z "$registry" ]]; then
|
||||||
echo "Error: DOCKER_REGISTRY (or --registry) is required." >&2
|
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
|
usage >&2
|
||||||
exit 2
|
exit 2
|
||||||
fi
|
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
|
echo "Error: repository must be namespace/name without leading or trailing slash: $repository" >&2
|
||||||
exit 2
|
exit 2
|
||||||
fi
|
fi
|
||||||
|
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||||
|
version_sh=$script_dir/version.sh
|
||||||
|
|
||||||
if [[ -z "$tag" ]]; then
|
if [[ -z "$tag" ]]; then
|
||||||
if [[ -n "$project_root" ]]; then
|
if [[ ! -x "$version_sh" ]]; then
|
||||||
tag=$(git -C "$project_root" describe --tags --always --dirty 2>/dev/null) || tag=
|
echo "Error: version.sh not found next to publish_docker.sh: $version_sh" >&2
|
||||||
|
exit 2
|
||||||
fi
|
fi
|
||||||
if [[ -z "$tag" ]]; then
|
if [[ -z "$project_root" ]]; then
|
||||||
echo "Error: IMAGE_TAG (or --tag) is required outside a git repository." >&2
|
echo "Error: IMAGE_TAG (or --tag) is required outside a git repository." >&2
|
||||||
exit 2
|
exit 2
|
||||||
fi
|
fi
|
||||||
|
tag=$("$version_sh" -C "$project_root" --docker) || {
|
||||||
|
echo "Error: failed to derive IMAGE_TAG from Git ancestry." >&2
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
fi
|
fi
|
||||||
if [[ "$tag" == *:* || "$tag" == */* ]]; then
|
if [[ "$tag" == *:* || "$tag" == */* ]]; then
|
||||||
echo "Error: tag must not contain : or /: $tag" >&2
|
echo "Error: tag must not contain : or /: $tag" >&2
|
||||||
exit 2
|
exit 2
|
||||||
fi
|
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
|
if [[ "$tag" == latest && ${ALLOW_LATEST:-0} != 1 && "$mode" == push ]]; then
|
||||||
echo "Error: refusing to publish floating tag 'latest'; pass an explicit version." >&2
|
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
|
echo "Set ALLOW_LATEST=1 only when the user explicitly asked for 'latest'." >&2
|
||||||
exit 3
|
exit 3
|
||||||
fi
|
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
|
if [[ "$mode" == load && "$platform" == *,* ]]; then
|
||||||
echo "Error: --load cannot be combined with multiple platforms: $platform" >&2
|
echo "Error: --load cannot be combined with multiple platforms: $platform" >&2
|
||||||
exit 2
|
exit 2
|
||||||
|
|||||||
Executable
+72
@@ -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"
|
||||||
@@ -15,9 +15,9 @@ Options:
|
|||||||
-p UPLOAD_PATH Override DEB_UPLOAD_PATH (default: /api/v2/upload/package)
|
-p UPLOAD_PATH Override DEB_UPLOAD_PATH (default: /api/v2/upload/package)
|
||||||
-h Show help
|
-h Show help
|
||||||
|
|
||||||
Environment variables may live in the project root .env; this script walks up
|
Environment variables may live in the project root .env.builder; this script
|
||||||
from the current directory, loads it silently (existing shell values win), and
|
loads it silently (existing shell values win), never echoes values, and does
|
||||||
never echoes variable values. The endpoint must accept multipart fields named
|
not read `.env`. The endpoint must accept multipart fields named
|
||||||
package, token, and repository_name. Authentication is read only from
|
package, token, and repository_name. Authentication is read only from
|
||||||
DEB_TOKEN so it is not exposed in the process command line.
|
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
|
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)
|
project_root=$(git rev-parse --show-toplevel 2>/dev/null || true)
|
||||||
|
|
||||||
# Load project .env without printing values; explicitly exported shell values keep precedence.
|
# Load .env.builder without printing values; shell values win. Do not read `.env`.
|
||||||
if [[ -n "$project_root" && -f "$project_root/.env" ]]; then
|
if [[ -n "$project_root" && -f "$project_root/.env.builder" ]]; then
|
||||||
while IFS='=' read -r key value; do
|
while IFS='=' read -r key value; do
|
||||||
key=${key%%[[:space:]]*}
|
key=${key%%[[:space:]]*}
|
||||||
[[ -z "$key" || "$key" == \#* ]] && continue
|
[[ -z "$key" || "$key" == \#* ]] && continue
|
||||||
if [[ -n "${!key:-}" ]]; then
|
if [[ -n "${!key:-}" ]]; then
|
||||||
continue # shell value already set: wins over .env
|
continue # shell value already set: wins over .env.builder
|
||||||
fi
|
fi
|
||||||
value=${value%\"}; value=${value#\"}; value=${value%\'}; value=${value#\'}
|
value=${value%\"}; value=${value#\"}; value=${value%\'}; value=${value#\'}
|
||||||
|
[[ -z "$value" ]] && continue
|
||||||
printf -v "$key" '%s' "$value"
|
printf -v "$key" '%s' "$value"
|
||||||
export "$key"
|
export "$key"
|
||||||
done < <(grep -v '^[[:space:]]*$' "$project_root/.env")
|
done < <(grep -v '^[[:space:]]*$' "$project_root/.env.builder")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
server_url=${DEB_SERVER_URL:-}
|
server_url=${DEB_SERVER_URL:-}
|
||||||
@@ -61,7 +62,7 @@ shift $((OPTIND - 1))
|
|||||||
|
|
||||||
if [[ -z "$server_url" || -z "$repository" || -z "$token" || $# -eq 0 ]]; then
|
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 "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
|
usage >&2
|
||||||
exit 2
|
exit 2
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -87,8 +87,10 @@ if dpkg-deb --control "$package" "$control_dir" 2>/dev/null; then
|
|||||||
for script in preinst postinst prerm postrm; do
|
for script in preinst postinst prerm postrm; do
|
||||||
if [[ -f "$control_dir/$script" ]]; then
|
if [[ -f "$control_dir/$script" ]]; then
|
||||||
found_scripts=true
|
found_scripts=true
|
||||||
mode=$(stat -c '%a' "$control_dir/$script")
|
# Use -x: octal last digit [2367] is "other write", so 755 (r-x) was
|
||||||
if [[ $mode =~ .*[2367]$ ]]; then
|
# 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"
|
echo "OK: $script mode $mode"
|
||||||
else
|
else
|
||||||
echo "FAIL: $script not executable (mode $mode)" >&2
|
echo "FAIL: $script not executable (mode $mode)" >&2
|
||||||
|
|||||||
@@ -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
|
||||||
Executable
+218
@@ -0,0 +1,218 @@
|
|||||||
|
#!/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)~YYYYMMDD.branch.n+gSHA
|
||||||
|
no tag no stable tag reachable from HEAD → 0.0.0~YYYYMMDD.branch+gSHA
|
||||||
|
|
||||||
|
YYYYMMDD is the build date. SOURCE_DATE_EPOCH, if set, is that instant in
|
||||||
|
UTC; otherwise the local calendar date. Official X.Y.Z never gets a date.
|
||||||
|
|
||||||
|
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-day YYYYMMDD. SOURCE_DATE_EPOCH (UTC) wins when set, for reproducible
|
||||||
|
# test packages; otherwise the machine's current local date.
|
||||||
|
build_date() {
|
||||||
|
if [[ -n "${SOURCE_DATE_EPOCH:-}" ]]; then
|
||||||
|
date -u -d "@${SOURCE_DATE_EPOCH}" +%Y%m%d
|
||||||
|
else
|
||||||
|
date +%Y%m%d
|
||||||
|
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 ymd
|
||||||
|
tag=$(official_tag_at_head)
|
||||||
|
if [[ -n "$tag" ]]; then
|
||||||
|
printf '%s' "${tag#v}"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
sha=$(short_sha)
|
||||||
|
branch=$(branch_slug)
|
||||||
|
ymd=$(build_date)
|
||||||
|
base=$(nearest_stable_tag)
|
||||||
|
if [[ -z "$base" ]]; then
|
||||||
|
printf '0.0.0~%s.%s+g%s' "$ymd" "$branch" "$sha"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
n=$(gitc rev-list --count "${base}..HEAD")
|
||||||
|
printf '%s~%s.%s.%s+g%s' "$(bump_patch "$base")" "$ymd" "$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
|
||||||
@@ -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)
|
||||||
@@ -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
@@ -5,31 +5,45 @@
|
|||||||
支持三种用法:
|
支持三种用法:
|
||||||
|
|
||||||
- **独立配置中心**:一个专门的 Git 仓库管所有机器的所有 Compose 服务(如 app00)
|
- **独立配置中心**:一个专门的 Git 仓库管所有机器的所有 Compose 服务(如 app00)
|
||||||
- **项目内 Compose 环境**:在普通项目里放 `.skiff/deployer/{prod,test,dev}/`,
|
- **项目内 Compose 环境**:在普通项目里放 `.pouch/deployer/{prod,test,dev}/`,
|
||||||
把这个项目的生产/测试/开发环境用 rsync + docker compose 部署
|
把这个项目的生产/测试/开发环境用 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),也可以指定本机已有目录。
|
你合并后由 Argo CD 同步。仓库既可以只写 Git 地址(部署时浅 clone),也可以指定本机已有目录。
|
||||||
|
|
||||||
## 什么时候使用
|
## 什么时候使用
|
||||||
|
|
||||||
|
- 给新项目初始化 deployer:检查缺什么,引导补 `.pouch/deployer/`
|
||||||
- 想用一套固定流程把本地改好的 Docker Compose 配置发到某台服务器
|
- 想用一套固定流程把本地改好的 Docker Compose 配置发到某台服务器
|
||||||
- 要升级某个服务的镜像版本、重启服务、看远程容器状态和日志
|
- 要升级某个服务的镜像版本、重启服务、看远程容器状态和日志
|
||||||
- 有编译好的 .deb 包要装到某台机器上(scp 上传安装,或从 URL 直接拉)
|
- 有编译好的 .deb 包要装到某台机器上(scp 上传安装,或从 URL 直接拉)
|
||||||
- 新加一个服务、把服务从一台机器挪到另一台、或下线旧服务
|
- 新加一个服务、把服务从一台机器挪到另一台、或下线旧服务
|
||||||
- 想给当前项目加 prod/test/dev 三套远程环境并随时部署其中一套
|
- 想给当前项目加 prod/test/dev 三套远程环境并随时部署其中一套
|
||||||
- ACK 说「重新布测试环境」或跑回归前要先拉起 `.skiff/deployer/test`
|
- ACK 说「重新布测试环境」或跑回归前要先拉起 `.pouch/deployer/test`
|
||||||
- 需要一张「哪台机器跑哪些服务」的清单
|
- 需要一张「哪台机器跑哪些服务」的清单
|
||||||
- 镜像要进 Kubernetes,走 Argo CD:改 GitOps、开 MR、合并后部署
|
- 镜像要进 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 服务的运行时数据放在哪(这些目录不能被同步覆盖)
|
- 知道每个 Compose 服务的运行时数据放在哪(这些目录不能被同步覆盖)
|
||||||
- Argo CD:能 clone/push GitOps 仓库(SSH 或已登录的 HTTPS),并能开 MR(GitLab `glab` / GitHub `gh` / Gitea `tea`)。本机不必长期放一份 GitOps checkout。
|
- Argo CD:能 clone/push GitOps 仓库(SSH 或已登录的 HTTPS),并能开 MR(GitLab `glab` / GitHub `gh` / Gitea `tea`)。本机不必长期放一份 GitOps checkout。
|
||||||
|
|
||||||
## 使用示例
|
## 使用示例
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
# 新项目
|
||||||
|
用 deployer 初始化这个项目
|
||||||
|
给这个项目接上 test 环境,SSH 别名是 my-vps
|
||||||
|
|
||||||
# 独立配置中心
|
# 独立配置中心
|
||||||
帮我把 vyyo1/naiveproxy 的配置改完部署上去
|
帮我把 vyyo1/naiveproxy 的配置改完部署上去
|
||||||
升级 vora3/gpt-load 的镜像版本
|
升级 vora3/gpt-load 的镜像版本
|
||||||
@@ -43,8 +57,9 @@ vhom1 上那个 naiveproxy 为什么 sync 失败?
|
|||||||
web1 能出网,直接让它从 https://... 把包拉下来装
|
web1 能出网,直接让它从 https://... 把包拉下来装
|
||||||
|
|
||||||
# 项目内环境
|
# 项目内环境
|
||||||
给这个项目建好 .skiff/deployer,prod 和 test 分别放到两台机器上
|
给这个项目建好 .pouch/deployer,prod 和 test 分别放到两台机器上
|
||||||
把 test 环境重新部署一下
|
把 test 环境重新部署一下
|
||||||
|
本机测试镜像已经有了,直接重启,别 sync
|
||||||
prod 的 compose 加个 redis,改完发上去
|
prod 的 compose 加个 redis,改完发上去
|
||||||
|
|
||||||
# Argo CD
|
# Argo CD
|
||||||
@@ -56,39 +71,40 @@ GitOps 我已经 clone 在 ../infra-gitops,用那个目录开 MR
|
|||||||
## Agent 会做什么
|
## Agent 会做什么
|
||||||
|
|
||||||
1. 读服务/环境目录(及共享的父目录)的 `_config.yaml`,确定目标机器和远程路径;
|
1. 读服务/环境目录(及共享的父目录)的 `_config.yaml`,确定目标机器和远程路径;
|
||||||
项目内布局从 `.skiff/deployer/` 自动发现,无需额外配置
|
项目内布局从 `.pouch/deployer/` 自动发现,无需额外配置
|
||||||
2. 用 skill 自带脚本把本地目录同步到远程(rsync,自动排除 `data/`、`_data/`)
|
2. 目标是本机且镜像已在本机 docker:不 sync、不 SSH,在该环境目录直接 `docker compose`
|
||||||
3. 在远程执行对应的 `docker compose` 操作(启动 / 重建 / 升级 / 重启)
|
3. 否则用 skill 自带脚本把本地目录同步到远程(优先 rsync,没有则 scp;自动排除 `data/`、`_data/`),再远程 `docker compose`(启动 / 重建 / 升级 / 重启)
|
||||||
4. deb 包安装走独立脚本:scp 上传到暂存目录后远程 apt 安装,失败自动修依赖
|
4. deb 包安装走独立脚本:scp 上传到暂存目录后远程 apt 安装,失败自动修依赖
|
||||||
5. 同步后查看容器状态和日志确认生效
|
5. 同步后查看容器状态和日志确认生效
|
||||||
6. 只针对你指定的那一个服务操作,不会批量动整台机器
|
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
|
不直接 kubectl 发布,不把 Harbor/TLS 密钥提交进 Git
|
||||||
|
|
||||||
项目内布局下,远程目录名自动带上项目前缀(如 `my-project-prod`),
|
项目内布局下,远程目录名自动带上项目前缀(如 `my-project-prod`),
|
||||||
避免同一台机器上多个项目的同名环境互相覆盖;需要固定名字时在 `_config.yaml` 写 `name:`。
|
避免同一台机器上多个项目的同名环境互相覆盖;需要固定名字时在 `_config.yaml` 写 `name:`。
|
||||||
|
|
||||||
重要边界:同步使用 `--delete`,远程多余的文件会被删除;数据库、证书等运行时数据
|
重要边界:rsync 同步使用 `--delete`,远程多余的文件会被删除;scp 兜底不会删。
|
||||||
必须放在排除目录或远程绝对路径挂载。涉及删除数据卷、清理远程文件的操作会先向你确认。
|
数据库、证书等运行时数据必须放在排除目录或远程绝对路径挂载。涉及删除数据卷、
|
||||||
|
清理远程文件的操作会先向你确认。
|
||||||
|
|
||||||
## 如何判断完成
|
## 如何判断完成
|
||||||
|
|
||||||
- 脚本输出显示同步完成、远程命令执行成功
|
- 本机:`docker compose ps` 容器 Up。远程:sync 成功且 `remote.py ps` 容器 Up
|
||||||
- `ps` 显示容器 Up、`logs` 无报错;升级后镜像 tag 与配置一致
|
- `logs` 无报错;升级后镜像 tag 与配置一致
|
||||||
- deb 安装后 `ssh <node> dpkg -l` 能看到目标包,服务能正常启动
|
- deb 安装后 `ssh <node> dpkg -l` 能看到目标包,服务能正常启动
|
||||||
- 域名/端口类服务能 curl 通
|
- 域名/端口类服务能 curl 通
|
||||||
- Argo CD:给出 MR 链接;合入后 Application Synced,域名/healthz 可访问
|
- Argo CD:给出 MR 链接;合入后 Application Synced,域名/healthz 可访问
|
||||||
|
|
||||||
## Argo CD 的两种接法
|
## 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. 只写仓库地址(默认)
|
### 1. 只写仓库地址(默认)
|
||||||
|
|
||||||
本机不用长期放 GitOps 仓库。部署时 Agent 浅 clone 到临时目录,改完开 MR,用完删掉。
|
本机不用长期放 GitOps 仓库。部署时 Agent 浅 clone 到临时目录,改完开 MR,用完删掉。
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# .skiff/deployer/argocd.yaml
|
# .pouch/deployer/argocd.yaml
|
||||||
repo: git@git.example.com:org/infra-gitops.git
|
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。
|
GitOps 仓库已经 checkout 在旁边时,写 `repo_dir`,Agent 直接进这个目录改、推分支、开 MR。
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# .skiff/deployer/argocd.yaml
|
# .pouch/deployer/argocd.yaml
|
||||||
repo: git@git.example.com:org/infra-gitops.git
|
repo: git@git.example.com:org/infra-gitops.git
|
||||||
repo_dir: ../infra-gitops
|
repo_dir: ../infra-gitops
|
||||||
```
|
```
|
||||||
|
|||||||
+55
-243
@@ -1,276 +1,88 @@
|
|||||||
---
|
---
|
||||||
name: deployer
|
name: deployer
|
||||||
description: >-
|
description: >-
|
||||||
管理两类部署:多机 Docker Compose(仓库存 compose.yaml 与静态配置,本 skill 脚本
|
初始化或检查项目部署配置;管理多机 Docker Compose(脚本同步后远程
|
||||||
同步到 SSH 节点后 docker compose 应用),以及 Argo CD GitOps(改 GitOps 仓库清单、
|
compose)与 Argo CD GitOps(改清单开 MR,用户合并后同步)。触发词:初始化
|
||||||
开 PR/MR,用户合并后由 Argo CD 同步)。当用户要求部署、同步、升级、重启远程
|
deployer、部署、sync、升级、装 deb、ArgoCD、GitOps;ACK 拉起测试环境时也可使用。
|
||||||
Compose 服务,向节点装 deb,新增/迁移/下线服务,梳理节点清单,make deploy TGT、
|
|
||||||
_config.yaml、rsync、tar over SSH、NAS 部署失败;或要求 ArgoCD / GitOps / K8s
|
|
||||||
部署、更新 Application、升镜像 tag、开 MR 让用户合并部署;或 ACK 要求拉起/
|
|
||||||
重布项目测试环境时使用。
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# deployer:Compose 节点与 Argo CD GitOps
|
# deployer:Compose 节点与 Argo CD GitOps
|
||||||
|
|
||||||
两条轨道,配置都在仓库里,方法由本 skill 提供。
|
|
||||||
|
|
||||||
- **Compose**:本地改 `compose.yaml` → 脚本同步到 SSH 节点 → 远程 `docker compose`。
|
- **Compose**:本地改 `compose.yaml` → 脚本同步到 SSH 节点 → 远程 `docker compose`。
|
||||||
- **Argo CD**:改 GitOps 仓库清单 → 开 PR/MR → 用户合并 → Argo CD 同步。不要用
|
- **Argo CD**:改 GitOps 仓库清单 → 开 PR/MR → 用户合并 → Argo CD 同步。不要用 Compose 的 `sync.py`/`remote.py` 去推集群。
|
||||||
Compose 的 `sync.py`/`remote.py` 去推集群。
|
|
||||||
|
|
||||||
---
|
开始时解析当前 `SKILL.md` 所在目录,记为 `<skill-dir>`。优先
|
||||||
|
`git rev-parse --show-toplevel` 解析项目根。不要创建 `.pouch/deployer/` 之外的
|
||||||
|
假配置中心,也不要用 `pouch init deployer`。
|
||||||
|
|
||||||
## 何时使用
|
## 选择模式
|
||||||
|
|
||||||
- 部署 / 同步 / 升级 / 重启某个远程 Docker Compose 服务
|
- 初始化、接入 deployer,或给新项目建测试/生产环境:执行「初始化」。
|
||||||
- 向节点安装 deb 包:scp 上传本地 .deb 后 dpkg/apt 安装,或从 URL 远程拉取安装
|
- 检查 `.pouch/deployer`、node、compose 是否齐全:执行「检查」。
|
||||||
- 新增、迁移、下线一个服务;梳理「哪台机器跑什么」
|
- 部署、重新部署、同步、升级、重启、布测试环境、装 deb、新增/下线服务:执行「Compose 操作」。发现不了服务或解析不出 node 时停止,转入「初始化」。不要静默初始化。
|
||||||
- sync 失败排查、证书丢失、改了配置不生效等运维问题
|
- ArgoCD / GitOps / 开 MR 部署:执行「Argo CD」。步骤见 [argocd.md](references/argocd.md)。
|
||||||
- 提到 `make deploy TGT=...`、`TGT=`、`_config.yaml`、rsync/tar 同步
|
- 两者都有且意图不清:先问。
|
||||||
- Argo CD / GitOps / 集群部署:新增 Application、改清单、升镜像 tag、开 MR 等用户合并
|
- 不适用:没有 `.pouch/deployer/` 的随手 `docker compose`、Nomad、常规 `kubectl apply`、构建并推送镜像(走 builder)。本机 node 仍走本 skill,镜像已在本机则跳过 sync。
|
||||||
- ACK Coordinator 拉起或重布项目测试环境(`.skiff/deployer/<env>`)
|
|
||||||
|
|
||||||
## 不适用
|
## 边界
|
||||||
|
|
||||||
- 单机 docker 日常使用(无多机同步诉求)
|
- 所有 sync/up/recreate/upgrade/restart 必须按单服务执行,禁止节点级批量。
|
||||||
- Nomad,或绕过 GitOps 用 `kubectl apply` 当常规发布
|
- `rsync` 带 `--delete`:运行时数据必须在 `data/`、`_data/` 或远程绝对路径挂载,否则会被清掉。
|
||||||
- 构建并推送镜像(走 builder);本 skill 只改 GitOps 里对该镜像的引用
|
- 仓库只放静态配置;证书、数据库、上传文件不进 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):
|
||||||
|
|
||||||
| 信号 | 轨道 |
|
```bash
|
||||||
|------|------|
|
python3 -I -S <skill-dir>/scripts/deploy/check.py --project <project-root>
|
||||||
| 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/
|
|
||||||
```
|
```
|
||||||
|
|
||||||
远程目录名 = 目录末级名:`vyyo1/naiveproxy` → `/opt/app/naiveproxy`。
|
6. 缺 `node` / compose / ssh 别名 = 部分完成或阻塞。不覆盖已有 compose/`_config.yaml`。除非用户明确要求,不部署、不提交。
|
||||||
|
|
||||||
**B. 项目内环境布局**:项目根放 `.skiff/deployer/{prod,test,dev}/`,
|
```text
|
||||||
每个环境一个目录。从项目内任意位置运行脚本即自动发现(也可用 `DEPLOYER_ROOT`
|
## deployer 初始化:完成 | 部分完成 | 阻塞
|
||||||
显式指定),无需环境变量:
|
|
||||||
|
|
||||||
```
|
已具备: …
|
||||||
my-project/
|
待配置: 路径 + 字段 + 可粘贴示例 + 缺了会挡住哪步
|
||||||
├── src/ ... # 项目本体
|
工具链: ssh / rsync(无 rsync 时 sync 用 scp 兜底,不擅自安装)
|
||||||
└── .skiff/deployer/
|
下一步: 一句话
|
||||||
├── _config.yaml # 三个环境共享默认(node/base_path 等)
|
|
||||||
├── argocd.yaml # 可选,Argo CD 指针(不是 compose 环境)
|
|
||||||
├── prod/
|
|
||||||
│ ├── compose.yaml # 生产 compose 与配置
|
|
||||||
│ └── _config.yaml # 环境级覆盖
|
|
||||||
├── test/compose.yaml
|
|
||||||
└── dev/compose.yaml
|
|
||||||
```
|
```
|
||||||
|
|
||||||
项目模式下远程目录名自动加项目前缀 `{git仓库名}-{env}`
|
## 检查
|
||||||
(如 `my-project-prod`),防止同主机多项目的同名环境互相覆盖;
|
|
||||||
`_config.yaml` 写 `name:` 可显式指定。
|
只读。运行 `scripts/deploy/check.py --project <project-root>`,用同一报告格式,标题改为 `## deployer 检查:…`。不写文件、不 SSH。用户明确要求修复后再转入初始化。
|
||||||
|
|
||||||
## 被 ACK 调用
|
## 被 ACK 调用
|
||||||
|
|
||||||
ACK 的「运行测试环境」和回归前布环境会加载本 skill,对项目
|
ACK 的「运行测试环境」和回归前布环境会加载本 skill,按「Compose 操作」的默认路径布测试环境。ACK 只负责何时布、把访问地址写入 `deliveryRuns`;不要把本 skill 的脚本复制进 ACK。生产环境、Argo CD 合入和节点级批量仍须用户明确要求。
|
||||||
`.skiff/deployer/<env>`(通常是 `test`)按下面 Compose 轨道执行。ACK 只负责何时
|
|
||||||
布、把访问地址写入 `deliveryRuns`;不要把本 skill 的脚本复制进 ACK。生产环境、
|
|
||||||
Argo CD 合入和节点级批量操作仍须用户明确要求,不能因为 ACK 调用就扩大范围。
|
|
||||||
|
|
||||||
## 步骤
|
## 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` 显式指定(独立配置中心仓库用这个)
|
新增/下线、deb、镜像 tag 升级、NAS 失败:再读 [compose.md](references/compose.md)。字段见 [config-reference.md](references/config-reference.md)。
|
||||||
2. 从当前目录向上找 `.skiff/deployer/`(项目内环境布局自动发现)
|
|
||||||
3. skill 安装位置兜底(仅用于查看,没有可部署服务)
|
|
||||||
|
|
||||||
```bash
|
## Argo CD
|
||||||
# <skill-dir> = 本 SKILL.md 所在目录,先解析出来记下
|
|
||||||
# 布局 A:显式指定仓库根
|
|
||||||
export DEPLOYER_ROOT=/path/to/your-compose-repo
|
|
||||||
python3 <skill-dir>/scripts/deploy/list.py
|
|
||||||
|
|
||||||
# 布局 B:在项目内直接跑即可(cwd 在项目里)
|
读 [argocd.md](references/argocd.md)。读 `.pouch/deployer/argocd.yaml`(无则只问 Git 地址写成 `repo:`)。按已有应用惯例改清单,从默认分支拉出分支开 MR。**停在 MR**,不合并、不 `kubectl apply` 工作负载。无 app-of-apps 时提醒用户首次 apply 那份 `application.yaml`。Harbor / TLS Secret 不入库。
|
||||||
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 |
|
|
||||||
|
|||||||
@@ -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 轨道**脚本解析,决定同步目标与排除规则。可放在**服务目录、部署根或其任意祖先目录**;子目录中的字段覆盖父目录(继承合并)。
|
`_config.yaml` 供 **Compose 轨道**脚本解析,决定同步目标与排除规则。可放在**服务目录、部署根或其任意祖先目录**;子目录中的字段覆盖父目录(继承合并)。
|
||||||
|
|
||||||
Argo CD 轨道用独立文件 `.skiff/deployer/argocd.yaml`,字段见 [argocd.md](argocd.md)。
|
Argo CD 轨道用独立文件 `.pouch/deployer/argocd.yaml`,字段见 [argocd.md](argocd.md)。
|
||||||
不要把 `argocd:` 嵌进本文件(脚本解析器不支持嵌套映射)。
|
不要把 `argocd:` 嵌进本文件(脚本解析器不支持嵌套映射)。
|
||||||
|
|
||||||
## 放置位置(两种布局)
|
## 放置位置(两种布局)
|
||||||
@@ -10,16 +10,16 @@ Argo CD 轨道用独立文件 `.skiff/deployer/argocd.yaml`,字段见 [argocd.
|
|||||||
| 布局 | 部署根 | `_config.yaml` 典型位置 |
|
| 布局 | 部署根 | `_config.yaml` 典型位置 |
|
||||||
|------|--------|------------------------|
|
|------|--------|------------------------|
|
||||||
| 独立配置中心仓库(`DEPLOYER_ROOT` 指向) | 仓库根 | 主机目录 `vyyo1/_config.yaml`、服务目录 |
|
| 独立配置中心仓库(`DEPLOYER_ROOT` 指向) | 仓库根 | 主机目录 `vyyo1/_config.yaml`、服务目录 |
|
||||||
| 项目内环境 `.skiff/deployer/{env}/` | `.skiff/deployer/` | 根级共享默认、各环境目录覆盖 |
|
| 项目内环境 `.pouch/deployer/{env}/` | `.pouch/deployer/` | 根级共享默认、各环境目录覆盖 |
|
||||||
|
|
||||||
项目布局示例:
|
项目布局示例:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# .skiff/deployer/_config.yaml — 三个环境共享
|
# .pouch/deployer/_config.yaml — 三个环境共享
|
||||||
node: my-vps
|
node: my-vps
|
||||||
base_path: /srv/apps
|
base_path: /srv/apps
|
||||||
|
|
||||||
# .skiff/deployer/prod/_config.yaml — 仅生产环境差异
|
# .pouch/deployer/prod/_config.yaml — 仅生产环境差异
|
||||||
node: prod-vps # 覆盖父级
|
node: prod-vps # 覆盖父级
|
||||||
name: my-project-api # 可选,覆盖默认的 {项目名}-{env}
|
name: my-project-api # 可选,覆盖默认的 {项目名}-{env}
|
||||||
```
|
```
|
||||||
@@ -111,7 +111,7 @@ sync_exclude:
|
|||||||
| 同一 SSH 主机多个服务 | 主机目录写一份 `node`/`base_path`,子服务免重复 |
|
| 同一 SSH 主机多个服务 | 主机目录写一份 `node`/`base_path`,子服务免重复 |
|
||||||
| 各服务目标不同 | 服务目录单独写 `_config.yaml` |
|
| 各服务目标不同 | 服务目录单独写 `_config.yaml` |
|
||||||
| 个别覆盖 | 子目录只写差异字段 |
|
| 个别覆盖 | 子目录只写差异字段 |
|
||||||
| 项目三环境同主机 | `.skiff/deployer/_config.yaml` 写共享 node,各环境只放差异 |
|
| 项目三环境同主机 | `.pouch/deployer/_config.yaml` 写共享 node,各环境只放差异 |
|
||||||
| 项目环境分属不同主机 | 各环境 `_config.yaml` 分别写 `node` |
|
| 项目环境分属不同主机 | 各环境 `_config.yaml` 分别写 `node` |
|
||||||
|
|
||||||
## 示例
|
## 示例
|
||||||
@@ -137,4 +137,4 @@ node: deploy@web2-backup
|
|||||||
- 目录内有 `compose.yaml`
|
- 目录内有 `compose.yaml`
|
||||||
- 能通过继承或兜底解析出 `node`
|
- 能通过继承或兜底解析出 `node`
|
||||||
- 独立仓库布局:路径中不含 `unused/` 段
|
- 独立仓库布局:路径中不含 `unused/` 段
|
||||||
- 项目环境布局:位于部署根 `.skiff/deployer/` 内(其外的项目文件不扫描)
|
- 项目环境布局:位于部署根 `.pouch/deployer/` 内(其外的项目文件不扫描)
|
||||||
|
|||||||
@@ -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())
|
||||||
@@ -31,7 +31,11 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
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,
|
project_root,
|
||||||
run_remote,
|
run_remote,
|
||||||
scp_base_args,
|
scp_base_args,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
支持两种布局:
|
支持两种布局:
|
||||||
1. 独立配置中心仓库(DEPLOYER_ROOT 指向,或 skill 安装位置)
|
1. 独立配置中心仓库(DEPLOYER_ROOT 指向,或 skill 安装位置)
|
||||||
2. 项目内环境目录 .skiff/deployer/{prod,test,dev}/(从 CWD 自动发现)
|
2. 项目内环境目录 .pouch/deployer/{prod,test,dev}/(从 CWD 自动发现;兼容 .skiff/deployer)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -18,10 +18,11 @@ DEFAULT_BASE_PATH = "/opt/app"
|
|||||||
DEFAULT_SYNC_EXCLUDES = ("data", "_data")
|
DEFAULT_SYNC_EXCLUDES = ("data", "_data")
|
||||||
_SKILL_DIR = Path(__file__).resolve().parent.parent # scripts/
|
_SKILL_DIR = Path(__file__).resolve().parent.parent # scripts/
|
||||||
PROJECT_ROOT: Path | None = None
|
PROJECT_ROOT: Path | None = None
|
||||||
|
_PROJECT_LAYOUT_DIRS = (".pouch", ".skiff")
|
||||||
|
|
||||||
|
|
||||||
def _find_project_root() -> Path:
|
def _find_project_root() -> Path:
|
||||||
"""部署根:DEPLOYER_ROOT > 从 CWD 向上找 .skiff/deployer > skill 安装位置。"""
|
"""部署根:DEPLOYER_ROOT > 从 CWD 向上找 .pouch/deployer > skill 安装位置。"""
|
||||||
env = os.environ.get("DEPLOYER_ROOT", "").strip()
|
env = os.environ.get("DEPLOYER_ROOT", "").strip()
|
||||||
if env:
|
if env:
|
||||||
p = Path(env).expanduser().resolve()
|
p = Path(env).expanduser().resolve()
|
||||||
@@ -31,9 +32,10 @@ def _find_project_root() -> Path:
|
|||||||
return p
|
return p
|
||||||
cur = Path.cwd()
|
cur = Path.cwd()
|
||||||
while True:
|
while True:
|
||||||
cand = cur / ".skiff" / "deployer"
|
for dirname in _PROJECT_LAYOUT_DIRS:
|
||||||
if cand.is_dir():
|
cand = cur / dirname / "deployer"
|
||||||
return cand
|
if cand.is_dir():
|
||||||
|
return cand
|
||||||
if cur == cur.parent:
|
if cur == cur.parent:
|
||||||
break
|
break
|
||||||
cur = cur.parent
|
cur = cur.parent
|
||||||
@@ -48,13 +50,13 @@ def project_root() -> Path:
|
|||||||
|
|
||||||
|
|
||||||
def in_project_layout(root: Path | None = None) -> bool:
|
def in_project_layout(root: Path | None = None) -> bool:
|
||||||
"""部署根是否为某项目内的 .skiff/deployer/。"""
|
"""部署根是否为某项目内的 .pouch/deployer/。"""
|
||||||
root = root or project_root()
|
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:
|
def project_display_name(root: Path | None = None) -> str:
|
||||||
"""项目名:git 仓库名优先,否则 .skiff 的父目录名。"""
|
"""项目名:git 仓库名优先,否则 .pouch 的父目录名。"""
|
||||||
root = root or project_root()
|
root = root or project_root()
|
||||||
anchor = root.parent.parent if in_project_layout(root) else root
|
anchor = root.parent.parent if in_project_layout(root) else root
|
||||||
try:
|
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]:
|
def config_paths_for_service(service_dir: str) -> list[Path]:
|
||||||
"""收集部署根自身及服务目录各层 _config.yaml(祖先在前,服务目录在后)。
|
"""收集部署根自身及服务目录各层 _config.yaml(祖先在前,服务目录在后)。
|
||||||
|
|
||||||
部署根的 _config.yaml(如 .skiff/deployer/_config.yaml)作为全局默认,
|
部署根的 _config.yaml(如 .pouch/deployer/_config.yaml)作为全局默认,
|
||||||
对所有环境/服务生效。
|
对所有环境/服务生效。
|
||||||
"""
|
"""
|
||||||
root = project_root()
|
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:
|
def is_deployable_dir(path: Path, root: Path) -> bool:
|
||||||
if "unused" in path.parts or "__pycache__" in path.parts:
|
if "unused" in path.parts or "__pycache__" in path.parts:
|
||||||
return False
|
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
|
return False
|
||||||
if not (path / "compose.yaml").is_file():
|
if not (path / "compose.yaml").is_file():
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -6,8 +6,13 @@ from __future__ import annotations
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from collections import defaultdict
|
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:
|
def main() -> int:
|
||||||
|
|||||||
@@ -20,8 +20,13 @@ import argparse
|
|||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
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 = {
|
REMOTE_COMMANDS = {
|
||||||
"up": "docker compose up -d",
|
"up": "docker compose up -d",
|
||||||
|
|||||||
@@ -16,15 +16,20 @@ import shlex
|
|||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
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:
|
def sync_directory(info: dict) -> int:
|
||||||
if shutil.which("rsync"):
|
if shutil.which("rsync"):
|
||||||
return _sync_rsync(info)
|
return _sync_rsync(info)
|
||||||
print("本机没有 rsync,改用 tar over SSH(不会删除远程多余文件)")
|
print("本机没有 rsync,改用 scp(不会删除远程多余文件)")
|
||||||
return _sync_tar(info)
|
return _sync_scp(info)
|
||||||
|
|
||||||
|
|
||||||
def _sync_rsync(info: dict) -> int:
|
def _sync_rsync(info: dict) -> int:
|
||||||
@@ -58,42 +63,81 @@ def _sync_rsync(info: dict) -> int:
|
|||||||
print(f"错误: rsync 执行失败,退出码: {e.returncode}")
|
print(f"错误: rsync 执行失败,退出码: {e.returncode}")
|
||||||
return 1
|
return 1
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
print("错误: rsync 命令未找到,请确保已安装 rsync")
|
print("本机没有 rsync,改用 scp(不会删除远程多余文件)")
|
||||||
return 1
|
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"]
|
source_dir = info["service_dir"]
|
||||||
remote_dir = info["remote_dir"]
|
remote_dir = info["remote_dir"]
|
||||||
tar_cmd = ["tar", "czf", "-", "-C", source_dir]
|
node = info["node"]
|
||||||
for item in info["sync_exclude"]:
|
source = Path(source_dir)
|
||||||
tar_cmd.append(f"--exclude={item}")
|
files = collect_sync_files(source_dir, info["sync_exclude"])
|
||||||
tar_cmd.append(".")
|
|
||||||
|
|
||||||
remote_shell = (
|
print(f"正在同步 {source_dir} 到 {node}:{remote_dir}")
|
||||||
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}")
|
|
||||||
if info["sync_exclude"]:
|
if info["sync_exclude"]:
|
||||||
print(f"排除目录: {', '.join(info['sync_exclude'])}")
|
print(f"排除目录: {', '.join(info['sync_exclude'])}")
|
||||||
print(f"执行命令: tar | {' '.join(ssh)}")
|
|
||||||
print("-" * 60)
|
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:
|
try:
|
||||||
completed = subprocess.run(ssh, stdin=tar.stdout, check=False)
|
completed = subprocess.run(mkdir_cmd, check=False)
|
||||||
finally:
|
except FileNotFoundError:
|
||||||
if tar.stdout:
|
print("错误: ssh 命令未找到")
|
||||||
tar.stdout.close()
|
return 1
|
||||||
tar.wait()
|
|
||||||
if tar.returncode:
|
|
||||||
print(f"错误: tar 打包失败,退出码: {tar.returncode}")
|
|
||||||
return tar.returncode
|
|
||||||
if completed.returncode:
|
if completed.returncode:
|
||||||
print(f"错误: 远程 tar 解包失败,退出码: {completed.returncode}")
|
print(f"错误: 远程 mkdir 失败,退出码: {completed.returncode}")
|
||||||
return 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("-" * 60)
|
||||||
print("同步完成!")
|
print("同步完成!")
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@@ -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,6 +8,13 @@
|
|||||||
- 已有 `.drawio` 原型,希望补齐跳转、异常态、权限或规则说明。
|
- 已有 `.drawio` 原型,希望补齐跳转、异常态、权限或规则说明。
|
||||||
- 需要一张能用于产品评审、开发对齐和测试设计的产品说明板。
|
- 需要一张能用于产品评审、开发对齐和测试设计的产品说明板。
|
||||||
|
|
||||||
|
## 安装
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pouch add draw-prototype-flow # 当前项目
|
||||||
|
pouch add draw-prototype-flow -g # 全局
|
||||||
|
```
|
||||||
|
|
||||||
## 使用前准备
|
## 使用前准备
|
||||||
|
|
||||||
最好提供产品目标、目标用户、核心任务和已有需求材料。缺少细节时,Agent 会先列出必要假设;只有关键范围无法判断时才会询问。
|
最好提供产品目标、目标用户、核心任务和已有需求材料。缺少细节时,Agent 会先列出必要假设;只有关键范围无法判断时才会询问。
|
||||||
@@ -34,4 +41,4 @@
|
|||||||
|
|
||||||
## 如何判断完成
|
## 如何判断完成
|
||||||
|
|
||||||
最终会得到可在 diagrams.net/Draw.io 打开的 `.drawio` 源文件。核心任务应能从入口走到结果,每个关键按钮有明确去向,异常状态有恢复动作,原型、动线和规格使用同一套页面与状态 ID。
|
最终会得到可在 diagrams.net/Draw.io 打开的 `.drawio` 源文件。你指定路径就用指定路径,没指定时 Agent 会在项目里给出文件名。核心任务应能从入口走到结果,每个关键按钮有明确去向,异常状态有恢复动作,原型、动线和规格使用同一套页面与状态 ID。
|
||||||
|
|||||||
@@ -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,不依赖外网资源。
|
||||||
|
- 画面上图大于字;没背景的人能看出「这东西怎么走」。
|
||||||
|
- 聊天没有再贴一遍长文解释。
|
||||||
@@ -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;聊天没有墙一样的解释。
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# host-sec-check
|
||||||
|
|
||||||
|
对一台 Linux/VPS 做入侵取向的安全检查:先取证,再给你一份结论和处置清单。默认不改机器。
|
||||||
|
|
||||||
|
## 什么时候用
|
||||||
|
|
||||||
|
- 「这台机做一次安全检查」
|
||||||
|
- 「好像被黑了 / 有矿机 / 服务无故 OOM」
|
||||||
|
- 「看看哪些端口对公网开着」
|
||||||
|
- 检查另一台机器时,直接点名主机:`对 vvps4 跑 /host-sec-check`
|
||||||
|
|
||||||
|
适用于单台 Linux(systemd + 常见 Docker/k3s)。不是应用代码审计,也不是云账号合规审计。
|
||||||
|
|
||||||
|
## 使用前准备
|
||||||
|
|
||||||
|
- SSH 能上(别名或 `user@host`)
|
||||||
|
- 尽量有 sudo 或 docker 组(没有也能做浅查)
|
||||||
|
- 告诉 Agent 哪张 IP 是公网,方便从外面探测
|
||||||
|
- 若要清理、关端口、重启:明确说「同意处置」
|
||||||
|
|
||||||
|
## 使用示例
|
||||||
|
|
||||||
|
```text
|
||||||
|
对 ace@vvps2 做 host-sec-check,只出报告先不要改。
|
||||||
|
```
|
||||||
|
|
||||||
|
```text
|
||||||
|
用 host-sec-check 查 vora3,公网 IP 是 x.x.x.x,发现未授权的就列处置项问我。
|
||||||
|
```
|
||||||
|
|
||||||
|
## Agent 会做什么
|
||||||
|
|
||||||
|
1. 采集账号、监听端口、cron/systemd、Docker/k3s、`ld.so.preload`、内存/hugepage。
|
||||||
|
2. 怀疑进程隐藏时,用容器读宿主机,避免被矿机库骗过。
|
||||||
|
3. 从另一台机器探测公网端口(含 IPv6,如果有)。
|
||||||
|
4. 给出结论:是否沦陷、入口、对外端口、建议处置。
|
||||||
|
5. 你点头后才停容器、占位矿机文件、收端口或重启。
|
||||||
|
|
||||||
|
## 如何判断完成
|
||||||
|
|
||||||
|
报告里有结论表;若做了处置,公网已关端口连不上,业务域名仍可用。密钥、token 不应出现在完整原文里。
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
---
|
||||||
|
name: host-sec-check
|
||||||
|
description: >-
|
||||||
|
Linux/VPS 主机安全检查与入侵取证:先取证再处置。覆盖矿机、ld.so.preload 进程隐藏、带宽挂机容器、对外端口、SSH/Docker/k8s 入口、cron/systemd 持久化、hugepage/OOM。
|
||||||
|
Use when: 安全检查, 被黑, 入侵, 矿机, 排查端口, 对公网放开, rpcbind, 挂机赚钱, Portainer, 安全审计, host security, compromise, cryptominer, exposure, /host-sec-check.
|
||||||
|
---
|
||||||
|
|
||||||
|
# host-sec-check
|
||||||
|
|
||||||
|
对一台 Linux 主机做安全检查。默认 **只读取证 + 出报告**。清理、关端口、重启、轮换密钥必须用户明确同意后再做。
|
||||||
|
|
||||||
|
需要:SSH 目标(`user@host` 或 ssh config 别名)、哪张网卡/IP 算公网。没有公网 IP 就只报「监听面」,并标明未做外部探测。
|
||||||
|
|
||||||
|
## 不适用
|
||||||
|
|
||||||
|
- Web 应用代码审计、依赖 CVE 扫描(可在报告里列为后续)
|
||||||
|
- 云厂商安全组控制台(能查就查,查不到就写「未验证」)
|
||||||
|
- 未获准的破坏性操作(`rm -rf`、整盘 wipe、强制重启)
|
||||||
|
|
||||||
|
## 铁律
|
||||||
|
|
||||||
|
1. **先取证,后动手。** 重启、删文件、停服务会毁掉证据。
|
||||||
|
2. **不要把密钥打进对话。** token、密码、authorized_keys、kubeconfig 只写「存在/已轮换/路径」,不贴原文。
|
||||||
|
3. **sudo 失败不要停。** 用户在 `docker` 组时,用特权容器 `nsenter` 看宿主机(见 [references/access.md](references/access.md))。
|
||||||
|
4. **怀疑进程隐藏时,不要只信宿主机 `ps`。** 从**不加载宿主机 `ld.so.preload` 的容器**读 `/proc` 和文件。
|
||||||
|
5. 宿主机 `chattr` 可能被隐藏库挡住;改 immutable 文件用 **Ubuntu/Alpine 自带 chattr** 操作 bind-mount 的 `/host/...`。
|
||||||
|
|
||||||
|
收集脚本(可选,先跑再解读):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash <skill-dir>/scripts/collect.sh USER@HOST
|
||||||
|
```
|
||||||
|
|
||||||
|
`<skill-dir>` 是本 skill 目录。脚本只采集,不修改系统。
|
||||||
|
|
||||||
|
## 流程
|
||||||
|
|
||||||
|
按顺序做。每步记下「正常 / 可疑 / 未查到」。完整项见 [references/checklist.md](references/checklist.md)。
|
||||||
|
|
||||||
|
### 1. 快照
|
||||||
|
|
||||||
|
- 主机名、发行版、uptime、负载、`free -h`、`df -h`、公网/内网/Tailscale IP
|
||||||
|
- `ss -lntup`(最好 root)
|
||||||
|
- `docker ps -a`、compose 项目
|
||||||
|
- 若有 k3s/k8s:agent 是否在跑、能否 `kubectl get pods -A`
|
||||||
|
|
||||||
|
### 2. 入口
|
||||||
|
|
||||||
|
- sshd:`Port`、`PermitRootLogin`、`PasswordAuthentication`、`authorized_keys`(root 与所有 UID≥1000)
|
||||||
|
- `last` / `lastb` / `journalctl -u ssh`:异常登录;**无交互登录却出现 docker run 也是入口信号**
|
||||||
|
- sudoers、`docker` 组(= root)、NOPASSWD
|
||||||
|
- 对公网的管理面:Portainer / Cockpit / 面板 / k8s apiserver / Docker 端口,尤其 `-p 0.0.0.0:…` 且挂了 `docker.sock` 或 `/:/host`
|
||||||
|
- 单元文件或 compose 里的明文 token(k3s `--token`、agent secret)
|
||||||
|
|
||||||
|
### 3. 持久化
|
||||||
|
|
||||||
|
读 [references/persistence.md](references/persistence.md)。至少查:
|
||||||
|
|
||||||
|
- crontab(root 与所有用户)、`/etc/cron.*`
|
||||||
|
- `/etc/systemd/system/*.service`、timers
|
||||||
|
- `/etc/ld.so.preload`(用**外部** cat/hexdump,宿主机 cat 可能被藏空)
|
||||||
|
- `chattr +i` 的异常二进制(`lsattr`)
|
||||||
|
- 隐藏 Docker 数据根(非 `/var/lib/docker` 的 dockerd `--data-root`)
|
||||||
|
|
||||||
|
### 4. 进程与隐藏库
|
||||||
|
|
||||||
|
- 从特权容器列 `/proc/*/cmdline`(绕过 preload)
|
||||||
|
- 对照宿主机 `ps`:只在容器视角出现的进程 = 隐藏
|
||||||
|
- `/proc/*/maps` 是否映射陌生 `.so`(`libgcwrap`、`libprocesshider`、随机名)
|
||||||
|
- 矿机常见名:`perfcc`、`xmrig`、`kdevtmpfsi`、`javavm64`;同体积多副本(例如都是 11MB)
|
||||||
|
- hugepage:`grep Huge /proc/meminfo`。`nr_hugepages>0` 且进程列表对不上,多半是矿机预留
|
||||||
|
|
||||||
|
### 5. 容器与编排
|
||||||
|
|
||||||
|
- `docker ps -a`:镜像名含 earnfm / packetstream / repocket / traffmonetizer / honeygain / proxyrack / bitping / antgain 等挂机池
|
||||||
|
- inspect:`Privileged`、Binds(`docker.sock`、`/:/host`)、Ports 是否 `0.0.0.0`
|
||||||
|
- 第二套 dockerd / 异常 `--bridge`
|
||||||
|
- k8s:节点上是否有与 Git 仓库无关的 workload;控制面不健康时从 kubelet `pods/*/etc-hosts` 读 Pod 名
|
||||||
|
|
||||||
|
### 6. 对外暴露
|
||||||
|
|
||||||
|
- 本机 `0.0.0.0` / `[::]` 监听
|
||||||
|
- **从另一台机器探测公网 IP**(含 IPv6)。只看 `ss` 会漏安全组,只看安全组会漏「监听但没防火墙」
|
||||||
|
- 高危:`rpcbind :111`、明文 HTTP 应用端口、Docker/Portainer、k8s 6443、数据库
|
||||||
|
- 管理 SSH 若可走 Tailscale,公网 SSH 算多余攻击面
|
||||||
|
|
||||||
|
### 7. 资源与日志
|
||||||
|
|
||||||
|
- 无 swap + 高 hugepage → 应用易 OOM
|
||||||
|
- `journalctl` / `auth.log` 在处置时间点附近;日志空也要记
|
||||||
|
- 出站连接:`ss -tp` 看陌生 IP(不要把结果里的密钥打出来)
|
||||||
|
|
||||||
|
### 8. 补漏(本次 vvps2 没做或做得浅的)
|
||||||
|
|
||||||
|
见 checklist 后半:补丁、SUID、世界可写、意外用户、IPv6、定时器、内核模块、出站 C2、云安全组、备份是否可用。
|
||||||
|
|
||||||
|
## 报告格式
|
||||||
|
|
||||||
|
先给结论(是否沦陷、最可能入口、对外还开着什么),再给证据表,最后给 **需用户点头的处置清单**。不要默默清理。
|
||||||
|
|
||||||
|
```
|
||||||
|
结论: 沦陷 / 可疑 / 未见异常
|
||||||
|
最可能入口: …
|
||||||
|
对外端口: …
|
||||||
|
持久化: …
|
||||||
|
隐藏/矿机: …
|
||||||
|
容器/编排: …
|
||||||
|
未查项: …
|
||||||
|
建议处置: (等确认)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 处置(仅在用户同意后)
|
||||||
|
|
||||||
|
原则见 [references/cleanup.md](references/cleanup.md)。
|
||||||
|
|
||||||
|
- 停并删除未授权容器;镜像也删
|
||||||
|
- 矿机二进制:删掉后用 **空文件 + `chattr +i` 占路径**
|
||||||
|
- 清空并锁定 `/etc/ld.so.preload`
|
||||||
|
- 关公网绑定:改成 `127.0.0.1` 或 Tailscale IP(若回源走 Tailscale 主机名,必须绑 TS IP,不能只绑 loopback)
|
||||||
|
- 停无用服务(如 `rpcbind.socket` + `rpcbind.service`,必要时 mask)
|
||||||
|
- 重启才能卸掉仍映射已删 `.so` 的老进程(sshd/dockerd)。先取证再重启
|
||||||
|
- 轮换:暴露过的 token、面板、k3s bootstrap。控制面 API 不通就不要假轮换,写阻塞原因
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
- 公网探测:已关端口失败;该留的业务域名仍 200
|
||||||
|
- `ss`:无 `0.0.0.0:已关端口`
|
||||||
|
- `grep -l libgcwrap /proc/*/maps`(或同类库)重启后为 0
|
||||||
|
- 占位文件仍在且 immutable
|
||||||
|
- 业务容器 `restart: always` 仍在
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# 取证入口(无 sudo 时)
|
||||||
|
|
||||||
|
目标用户常见情况:`sudo` 要密码,但在 `docker` 组。Docker 组等价于 root。
|
||||||
|
|
||||||
|
## 读宿主机(绕过 preload)
|
||||||
|
|
||||||
|
容器用自己的 libc,**不会**加载宿主机 `/etc/ld.so.preload`。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm -i --privileged --pid=host --net=host -v /:/host alpine:latest \
|
||||||
|
cat /host/etc/ld.so.preload
|
||||||
|
hexdump -C /host/etc/ld.so.preload # 在容器内
|
||||||
|
```
|
||||||
|
|
||||||
|
列隐藏进程:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm --privileged --pid=host alpine:latest sh -c \
|
||||||
|
'for d in /proc/[0-9]*; do
|
||||||
|
pid=${d#/proc/}
|
||||||
|
cmd=$(tr "\0" " " < "$d/cmdline" 2>/dev/null) || continue
|
||||||
|
[ -n "$cmd" ] && echo "$pid $cmd"
|
||||||
|
done'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 改宿主机(systemctl / chattr)
|
||||||
|
|
||||||
|
`chroot /host /usr/bin/chattr` 会加载宿主机 preload,可能 **stat 不到** 被藏文件。
|
||||||
|
|
||||||
|
用发行版自带 chattr 操作 `/host/...`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm -i --privileged --pid=host --net=host -v /:/host ubuntu:latest bash -s <<'SH'
|
||||||
|
chattr -i /host/etc/ld.so.preload
|
||||||
|
: > /host/etc/ld.so.preload
|
||||||
|
chattr +i /host/etc/ld.so.preload
|
||||||
|
lsattr /host/etc/ld.so.preload
|
||||||
|
SH
|
||||||
|
```
|
||||||
|
|
||||||
|
systemctl / reboot:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm --privileged --pid=host --net=host alpine:latest \
|
||||||
|
nsenter -t 1 -m -u -i -n -- systemctl disable --now UNIT
|
||||||
|
```
|
||||||
|
|
||||||
|
不要用业务容器当跳板;用 `alpine`/`ubuntu` 临时 `--rm`。查到一半时 `docker ps` 里出现的随机名容器就是这个跳板。
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user