feat(ack): add project knowledge guardrails
This commit is contained in:
+52
-5
@@ -37,7 +37,8 @@ skiff init ack --project ~/code/my-app
|
||||
```text
|
||||
docs/ack/
|
||||
├── project.md
|
||||
└── tasks.yaml
|
||||
├── tasks.yaml
|
||||
└── knowledge.yaml
|
||||
```
|
||||
|
||||
不会在项目中复制或链接 ACK Skill。通用规范、模板和脚本始终从已安装的 Skill
|
||||
@@ -51,23 +52,69 @@ skills/ack/
|
||||
├── README.md
|
||||
├── VERSION
|
||||
├── references/ # 三角色规范、闭环流程和初始化说明
|
||||
├── templates/ # project.md 与 tasks.yaml 模板和 schema
|
||||
├── templates/ # project.md、tasks.yaml、knowledge.yaml 模板和 schema
|
||||
├── examples/ # 完整示例
|
||||
└── scripts/ # tasks.yaml 与 worker 命令校验器
|
||||
└── scripts/ # 状态校验、知识选择、安全验证执行与 worker 命令工具
|
||||
```
|
||||
|
||||
`SKILL.md` 是 Agent 的工作流入口。`references/` 是按需读取的稳定规范;
|
||||
`docs/ack/project.md` 只保存当前项目的命令、路径和权限差异;
|
||||
`docs/ack/tasks.yaml` 保存当前任务状态。
|
||||
`docs/ack/tasks.yaml` 保存当前任务状态;`docs/ack/knowledge.yaml` 保存跨任务复用、
|
||||
已经独立验证的项目知识护栏。
|
||||
|
||||
## 检查任务板
|
||||
## 检查项目状态
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
Coordinator 可以按当前任务上下文做确定性推荐:
|
||||
|
||||
```bash
|
||||
python3 <ack-skill-dir>/scripts/select_knowledge.py docs/ack/knowledge.yaml \
|
||||
--component web --path web/app.py --tag long-running-service --limit 10
|
||||
```
|
||||
|
||||
默认 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`。知识正文不能作为自由 shell
|
||||
执行;关键约束应继续下沉到测试、lint、CI 或正式规范。ACK 不自动修改项目的
|
||||
`AGENTS.md`、`CLAUDE.md` 或其它 Agent 指令文件。
|
||||
|
||||
## 开始一个需求
|
||||
|
||||
初始化完成后可以直接说:
|
||||
|
||||
+48
-17
@@ -9,8 +9,8 @@ description: >-
|
||||
# ACK 项目协作入口
|
||||
|
||||
本 Skill 是 ACK 的完整能力包:`references/` 保存通用规范,`templates/` 保存项目
|
||||
状态模板,`scripts/` 保存校验工具。目标项目只在 `docs/ack/` 保存 `project.md` 和
|
||||
`tasks.yaml`,不要复制或链接 Skill 内容。
|
||||
状态模板,`scripts/` 保存校验工具。目标项目只在 `docs/ack/` 保存 `project.md`、
|
||||
`tasks.yaml` 和 `knowledge.yaml`,不要复制或链接 Skill 内容。
|
||||
|
||||
开始时解析当前 `SKILL.md` 所在目录,记为 `<ack-skill-dir>`。所有通用规范、模板和
|
||||
脚本都相对此目录访问,不依赖固定的全局安装路径。
|
||||
@@ -36,8 +36,12 @@ description: >-
|
||||
|
||||
该命令从本 Skill 的 `templates/` 生成项目状态,不会在项目中创建 Skill
|
||||
软链接或资源副本。
|
||||
3. 如果 `docs/ack` 已存在,不重复初始化、不覆盖文件;转入“检查”,报告缺失项并
|
||||
只补用户授权且能安全确定的内容。
|
||||
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`:
|
||||
@@ -47,15 +51,21 @@ description: >-
|
||||
- 只写项目差异,不复制 `references/` 中的通用规范。
|
||||
6. 完善 `docs/ack/tasks.yaml` 的项目信息。纯初始化且用户没有提供真实任务时,
|
||||
删除模板示例任务并保留 `tasks: []`;不要虚构需求或缺陷。
|
||||
7. 更新 `updatedAt`,并运行:
|
||||
7. 检查 `docs/ack/knowledge.yaml`。新项目没有已验证的项目经验时保留
|
||||
`verificationRegistry: {}` 与 `entries: []`,不从聊天、README 或单次失败中
|
||||
猜测并激活知识。
|
||||
8. 更新 `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
|
||||
```
|
||||
|
||||
8. 检查 `project.md` 与 `tasks.yaml` 是否仍有 `<...>` 占位符。结构校验通过且
|
||||
必填项目事实完整时才称“初始化完成”;否则称“部分完成”并列出缺失值。
|
||||
9. 报告创建的路径、检测到的命令、校验结果和下一步。除非用户明确要求,不提交、
|
||||
9. 检查 `project.md`、`tasks.yaml` 与 `knowledge.yaml` 是否仍有 `<...>` 占位符。
|
||||
结构校验通过且必填项目事实完整时才称“初始化完成”;否则称“部分完成”并列出
|
||||
缺失值。
|
||||
10. 报告创建的路径、检测到的命令、校验结果和下一步。除非用户明确要求,不提交、
|
||||
不推送。
|
||||
|
||||
## 检查
|
||||
@@ -63,13 +73,18 @@ description: >-
|
||||
1. 检查以下路径:
|
||||
- `docs/ack/project.md`
|
||||
- `docs/ack/tasks.yaml`
|
||||
- `docs/ack/knowledge.yaml`
|
||||
2. 读取 `<ack-skill-dir>/VERSION`,对比 `tasks.yaml` 的 `ackVersion`。旧项目只有
|
||||
`ackVersion` 时仍可读取,但建议迁移为 `ackVersion`。
|
||||
`kitVersion` 时仍可读取,但建议迁移为 `ackVersion`。
|
||||
3. 查找未替换占位符,并核对项目路径、覆盖层路径、Developer 白盒命令、Test
|
||||
黑盒命令和 Base URL。
|
||||
4. 使用 `<ack-skill-dir>/scripts/validate_tasks.py` 校验任务板。只报告证据明确的
|
||||
问题,不因可选字段缺失而宣称失败。
|
||||
5. 检查不会自动修复或覆盖现有配置;用户明确要求修复后再修改。
|
||||
4. 使用 `<ack-skill-dir>/scripts/validate_tasks.py` 校验任务板,使用
|
||||
`<ack-skill-dir>/scripts/validate_knowledge.py docs/ack/knowledge.yaml --tasks
|
||||
docs/ack/tasks.yaml` 校验项目知识和跨文件引用。只报告证据明确的问题,不因可选
|
||||
字段缺失而宣称失败。
|
||||
5. 检查知识引用能解析到固定 revision,candidate 仍留在任务证据中,且
|
||||
`stale`、`superseded` 和 `archived` 不会被当作可派发的 `active` 知识。
|
||||
6. 检查不会自动修复或覆盖现有配置;用户明确要求修复后再修改。
|
||||
|
||||
## 工作
|
||||
|
||||
@@ -77,16 +92,28 @@ description: >-
|
||||
2. 依次读取:
|
||||
- `docs/ack/project.md`
|
||||
- `docs/ack/tasks.yaml`
|
||||
- 通过 `<ack-skill-dir>/scripts/select_knowledge.py` 从
|
||||
`docs/ack/knowledge.yaml` 选择的当前任务相关 `active` 条目
|
||||
- `<ack-skill-dir>/references/kickoff.md`
|
||||
- kickoff 指定且与当前任务相关的 references 文件
|
||||
3. 当前会话担任 Coordinator,遵守项目覆盖层中的命令、路径权限、模型路由和
|
||||
worker 复用规则。项目覆盖层优先于通用示例命令。
|
||||
worker 复用规则。项目覆盖层优先于通用示例命令。按 scope 推荐相关 `active`
|
||||
知识,经确认后把固定 revision 的显式 `knowledgeRefs` 写入当前任务上下文;
|
||||
不全量注入知识库。
|
||||
4. 新需求先写产品文档、任务拆分与可观测验收信号,更新 `tasks.yaml` 并校验,
|
||||
然后交给用户确认;确认前不派发实现。
|
||||
5. 用户已确认的任务按 ACK 闭环执行:Developer 实现与白盒验证,Test 独立黑盒
|
||||
复测,Coordinator 读取证据终检并唯一写入 `tasks.yaml`。
|
||||
6. 不把 `worker_done` 或 Test 自报成功直接当作完成。每项最多三轮,仍失败则记录
|
||||
复测,Coordinator 读取证据终检并唯一写入 `tasks.yaml`。Developer 回报
|
||||
`knowledgeApplied` 和 `knowledgeCandidates`,Test 回报 `knowledgeChecks`;
|
||||
`candidate` 只有在独立验证和 gate 后才能由 Coordinator 写入或激活。
|
||||
6. 执行知识项的 `verification.ref` 时,只调用
|
||||
`<ack-skill-dir>/scripts/run_verification.py docs/ack/knowledge.yaml
|
||||
<verification-ref> --project-root <project-root>`。不要直接执行选择器返回的 path/args,
|
||||
也不要给 runner 注入额外命令或参数。
|
||||
7. 不把 `worker_done` 或 Test 自报成功直接当作完成。每项最多三轮,仍失败则记录
|
||||
`leftover` 并继续其它任务。
|
||||
8. 关键的安全、正确性和兼容性约束应下沉为测试、lint、CI 或正式规范;
|
||||
`knowledge.yaml` 只保存触发条件、原因与证据引用,不能替代可执行控制。
|
||||
|
||||
## 边界
|
||||
|
||||
@@ -94,5 +121,9 @@ description: >-
|
||||
- 不在项目中维护第二份 ACK 通用规范、模板或任务 schema。
|
||||
- 不猜测项目命令、服务地址、worker handle 或模型名称。
|
||||
- 不覆盖已有 `docs/ack` 文件,不擅自提交、推送、创建终端或新 worktree。
|
||||
- 项目只保存 `docs/ack/project.md` 和 `docs/ack/tasks.yaml`;通用资源始终从当前
|
||||
ACK Skill 目录读取。
|
||||
- 只有 Coordinator 写 `tasks.yaml` 和 `knowledge.yaml`;Developer 与 Test 只读,
|
||||
只能通过回报提名或验证知识。
|
||||
- 不把知识正文或选择器输出拼成 shell;知识检查只能通过 `run_verification.py`
|
||||
按 registry ID 执行。不自动修改 `AGENTS.md`、`CLAUDE.md` 或其它 Agent 指令文件。
|
||||
- 项目只保存 `docs/ack/project.md`、`docs/ack/tasks.yaml` 和
|
||||
`docs/ack/knowledge.yaml`;通用资源始终从当前 ACK Skill 目录读取。
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
0.8.1
|
||||
0.9.0
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
version: 1
|
||||
updatedAt: "2026-07-31T12:00:00+08:00"
|
||||
project:
|
||||
name: "notes-web"
|
||||
verificationRegistry:
|
||||
service-worktree-alignment:
|
||||
path: "tests/ack/check_service_worktree.py"
|
||||
args: ["--require-current-commit"]
|
||||
entries:
|
||||
- id: "K-001"
|
||||
revision: 1
|
||||
kind: "verification"
|
||||
status: "active"
|
||||
title: "复测前确认服务与 worktree/commit 对齐"
|
||||
subject: "service-worktree-alignment"
|
||||
scope:
|
||||
all: false
|
||||
components: ["web"]
|
||||
paths: ["web/**"]
|
||||
dependencies: []
|
||||
versions: []
|
||||
tags: ["long-running-service"]
|
||||
symbols: []
|
||||
errorSignatures: []
|
||||
appliesWhen: "修改常驻 Web 服务或前端构建产物"
|
||||
directive: "复测前重启服务,并核对服务实例对应的 worktree 与 commit"
|
||||
rationale: "历史任务曾因 Test 连接旧进程而产生假通过"
|
||||
verification:
|
||||
ref: "service-worktree-alignment"
|
||||
expected: "服务实例、worktree 和 commit 一致"
|
||||
provenance:
|
||||
taskId: "BUG-002"
|
||||
attemptId: "BUG-002-A2"
|
||||
codeRef: "abc1234"
|
||||
evidenceRef: "tasks.yaml#BUG-002"
|
||||
owner: "web-team"
|
||||
author: "developer"
|
||||
reviewer: "test"
|
||||
approval: null
|
||||
createdAt: "2026-07-30T10:00:00+08:00"
|
||||
lastValidatedAt: "2026-07-31T11:00:00+08:00"
|
||||
reviewAfter: "2099-10-31T11:00:00+08:00"
|
||||
temporary: false
|
||||
removalCondition: null
|
||||
statusReason: null
|
||||
supersedes: []
|
||||
conflictsWith: []
|
||||
@@ -1,9 +1,10 @@
|
||||
# notes-web Agent 协作协议(示例,项目覆盖层)
|
||||
|
||||
> 本项目基于 ack v0.8.1。
|
||||
> 本项目基于 ack v0.9.0。
|
||||
> 通用规范由 `/ack` 从 Skill 自身的 `references/` 读取,本文件只填项目差异。
|
||||
> 覆盖层文件放在 `docs/ack/project.md`,不占用 `AGENTS.md`。
|
||||
> `docs/ack/` 只保存 `project.md` 与 `tasks.yaml`。
|
||||
> ACK 不会自动修改 `AGENTS.md`、`CLAUDE.md` 或其它 Agent 指令文件。
|
||||
> `docs/ack/` 只保存 `project.md`、`tasks.yaml` 与 `knowledge.yaml`。
|
||||
|
||||
## 项目概览
|
||||
|
||||
@@ -12,6 +13,7 @@
|
||||
- 运行命令:`npm run dev`(前端)、`go run ./server`(后端)
|
||||
- Base URL:`http://localhost:5173`
|
||||
- 任务板:`docs/ack/tasks.yaml`
|
||||
- 项目知识:`docs/ack/knowledge.yaml`
|
||||
- 覆盖层文件:`docs/ack/project.md`
|
||||
|
||||
## 稳定规范(引用,不重复)
|
||||
@@ -44,6 +46,7 @@
|
||||
| `config/*.example.*` | Read-only | Read-only | R/W | 可提交配置模板 |
|
||||
| `.env`、`config/local.*` | Read-only | Read-only | Read-only | 本地私有配置 |
|
||||
| `tasks.yaml` | R/W | Read-only | Read-only | 只有 Coordinator 写 |
|
||||
| `knowledge.yaml` | R/W | Read-only | Read-only | 只有 Coordinator 写;Developer/Test 通过回报提名或验证 |
|
||||
|
||||
## 命令
|
||||
|
||||
@@ -63,13 +66,22 @@ curl -s -X POST http://localhost:5173/api/fix/preview -d @fixtures/preview.json
|
||||
# 浏览器回归:tests/browser/cases/*.md
|
||||
```
|
||||
|
||||
任务板校验由 `/ack` 使用 Skill 自带的 `scripts/validate_tasks.py` 执行。
|
||||
项目已审查的知识检查入口保存在 `knowledge.yaml.verificationRegistry`,格式为检查
|
||||
ID 对应仓库内相对 path 和结构化 args。知识正文不保存或自动执行自由 shell 命令。
|
||||
执行时只把检查 ID 交给 Skill 的 `scripts/run_verification.py`,不直接拼接
|
||||
path/args。
|
||||
项目状态校验由 `/ack` 使用 Skill 自带的 `scripts/validate_tasks.py` 和
|
||||
`scripts/validate_knowledge.py` 执行。
|
||||
|
||||
## 硬规则(其余见 references/)
|
||||
|
||||
- 三角色独立:Coordinator 只编排、Test 只验证、Developer 只实现。
|
||||
- 模型分层:Coordinator 强模型不跑测试,Test/Developer 中低模型(见 references/model-routing.md)。
|
||||
- `worker_done` 与复测报告都不等于完成,必须 Test 独立复测 + Coordinator 终检后才能 `verified`。
|
||||
- 只有 Coordinator 写 `tasks.yaml`;Test 与 Developer 只读。
|
||||
- 只有 Coordinator 写 `tasks.yaml` 和 `knowledge.yaml`;Test 与 Developer 只读。
|
||||
- Coordinator 只派发按 scope 命中并显式写入 `knowledgeRefs` 的 `active` 知识;
|
||||
`candidate` 不派发,知识库不全量注入。
|
||||
- Developer 回报 `knowledgeApplied` 与 `knowledgeCandidates`,Test 回报
|
||||
`knowledgeChecks`;关键约束应继续下沉到测试、lint、CI 或正式规范。
|
||||
- 每个任务最多派发 3 轮,仍不过标记 `leftover` 并继续。
|
||||
- 不提交或推送,除非用户明确要求。
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
version: 1
|
||||
updatedAt: "2026-07-06T09:40:00+08:00"
|
||||
source: "Coordinator (PM) Agent"
|
||||
ackVersion: "0.8.1"
|
||||
ackVersion: "0.9.0"
|
||||
project:
|
||||
name: "notes-web"
|
||||
repoPath: "/home/dev/notes-web"
|
||||
baseUrl: "http://localhost:5173"
|
||||
devWorktree: "/home/dev/notes-web-wt/fix-preview"
|
||||
overlayFile: "docs/ack/project.md"
|
||||
knowledgeFile: "docs/ack/knowledge.yaml"
|
||||
|
||||
summary:
|
||||
verified: ["BUG-002"]
|
||||
@@ -27,6 +29,19 @@ tasks:
|
||||
- "docs/spec/fix-preview.md#preview"
|
||||
testRefs:
|
||||
- "tests/browser/cases/01-preview.md"
|
||||
knowledgeRefs:
|
||||
- "K-001@1"
|
||||
knowledgeApplied:
|
||||
- ref: "K-001@1"
|
||||
result: "applied"
|
||||
evidence: "重启服务并核对 commit 后再交给 Test"
|
||||
knowledgeCandidates: []
|
||||
knowledgeChecks:
|
||||
- ref: "K-001@1"
|
||||
result: "passed"
|
||||
evidence: "服务实例、worktree 与 commit 9f8e7d6 一致"
|
||||
checkedBy: "test-worker-1"
|
||||
checkedAt: "2026-07-06T09:36:00+08:00"
|
||||
description: >
|
||||
用户在 /fix 页点击“预览变更”后,确认区不渲染 API 返回的 diff。
|
||||
stepsToReproduce:
|
||||
@@ -55,9 +70,11 @@ tasks:
|
||||
worker: "dev-worker-1"
|
||||
rounds:
|
||||
- round: 1
|
||||
attemptId: "BUG-002-A1"
|
||||
result: failed
|
||||
evidence: "只渲染了 title,缺少 before/after/coverChanged"
|
||||
- round: 2
|
||||
attemptId: "BUG-002-A2"
|
||||
result: passed
|
||||
evidence: "复测 4 行 diff 全部出现,取消不触发写入"
|
||||
resolution:
|
||||
@@ -76,6 +93,24 @@ tasks:
|
||||
specRefs:
|
||||
- "docs/spec/concurrency.md"
|
||||
testRefs: []
|
||||
knowledgeRefs: []
|
||||
knowledgeApplied: []
|
||||
knowledgeCandidates:
|
||||
- kind: "pitfall"
|
||||
title: "乐观锁检查必须覆盖批量导入路径"
|
||||
claim: "只在 API 写入路径检查版本号会漏掉批量导入"
|
||||
scope:
|
||||
components: ["server-store"]
|
||||
paths: ["server/store/**"]
|
||||
tags: ["concurrency"]
|
||||
appliesWhen: "修改 note 的并发写入或批量导入逻辑"
|
||||
directive: "同时验证 API 与批量导入的版本冲突处理"
|
||||
rationale: "第二轮修复只覆盖 API,批量导入仍可静默覆盖"
|
||||
evidenceRefs:
|
||||
- "tasks.yaml#BUG-003-round-2"
|
||||
proposedBy: "test-worker-1"
|
||||
proposedAt: "2026-07-06T09:32:00+08:00"
|
||||
knowledgeChecks: []
|
||||
description: >
|
||||
两个会话同时保存同一条 note 时,后写覆盖先写,无冲突提示。
|
||||
stepsToReproduce:
|
||||
@@ -100,12 +135,15 @@ tasks:
|
||||
worker: "dev-worker-1"
|
||||
rounds:
|
||||
- round: 1
|
||||
attemptId: "BUG-003-A1"
|
||||
result: failed
|
||||
evidence: "加了版本号但未在写入路径校验"
|
||||
- round: 2
|
||||
attemptId: "BUG-003-A2"
|
||||
result: failed
|
||||
evidence: "校验只覆盖 API,未覆盖批量导入路径"
|
||||
- round: 3
|
||||
attemptId: "BUG-003-A3"
|
||||
result: failed
|
||||
evidence: "乐观锁与前端重试逻辑冲突,需重新设计"
|
||||
resolution:
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
|
||||
- [ ] ACK Skill 已全局安装或安装到当前项目。
|
||||
- [ ] 已运行 `skiff init ack --project <project-root>`。
|
||||
- [ ] `docs/ack/` 只包含项目自己的 `project.md` 与 `tasks.yaml`。
|
||||
- [ ] `docs/ack/` 只包含项目自己的 `project.md`、`tasks.yaml` 与 `knowledge.yaml`。
|
||||
- [ ] 旧项目缺少 `knowledge.yaml` 时,只补空文件及缺失的
|
||||
`project.knowledgeFile` 指针,没有重跑初始化或覆盖其它项目状态。
|
||||
- [ ] 项目中没有 ACK Skill 的复制目录或 `kit`、`framework` 软链接。
|
||||
- [ ] `tasks.yaml` 使用 `ackVersion` 记录 ACK Skill 版本。
|
||||
|
||||
@@ -12,6 +14,8 @@
|
||||
|
||||
- [ ] `project.md` 只保存项目差异,不复制 Skill 的通用规范。
|
||||
- [ ] `tasks.yaml` 的 `project.overlayFile` 指向实际覆盖层。
|
||||
- [ ] `tasks.yaml` 的 `project.knowledgeFile` 固定为
|
||||
`docs/ack/knowledge.yaml`。
|
||||
- [ ] 技术栈、运行、构建、单测和集成测试命令均来自项目证据。
|
||||
- [ ] Coordinator、Developer、Test 的模型档位和升级规则已明确。
|
||||
|
||||
@@ -22,6 +26,7 @@
|
||||
- [ ] Developer 可写源码与单元测试,但不能改规格或黑盒验收。
|
||||
- [ ] 私有配置只读且不提交。
|
||||
- [ ] `tasks.yaml` 只有 Coordinator 写。
|
||||
- [ ] `knowledge.yaml` 只有 Coordinator 写;Developer 与 Test 只通过回报提名或验证。
|
||||
|
||||
## 任务板
|
||||
|
||||
@@ -30,12 +35,30 @@
|
||||
- [ ] 真实任务的验收是可观测信号。
|
||||
- [ ] 已运行 `<ack-skill-dir>/scripts/validate_tasks.py` 并通过。
|
||||
|
||||
## 项目知识
|
||||
|
||||
- [ ] 新项目没有已验证知识时使用 `verificationRegistry: {}` 与 `entries: []`,
|
||||
不虚构 active 条目。
|
||||
- [ ] 已运行 `<ack-skill-dir>/scripts/validate_knowledge.py` 并通过;同时校验任务引用。
|
||||
- [ ] Coordinator 只从 `active` 条目按 scope 推荐知识,并显式确认固定 revision 的
|
||||
`knowledgeRefs`。
|
||||
- [ ] `candidate` 留在任务证据中,不派发;`stale`、`superseded`、`archived`
|
||||
不默认选择。
|
||||
- [ ] 每轮只内联命中的少量知识,不全量注入 `knowledge.yaml`。
|
||||
- [ ] Developer 回报 `knowledgeApplied` 与 `knowledgeCandidates`,Test 回报
|
||||
`knowledgeChecks`。
|
||||
- [ ] `verification.ref` 只引用 `verificationRegistry` 中已审查的仓库内相对 path
|
||||
和结构化 args;执行时只把 registry ID 交给 `run_verification.py`,不从
|
||||
知识正文或选择器输出拼接 shell。
|
||||
- [ ] 关键约束已规划下沉到测试、lint、CI 或正式规范。
|
||||
- [ ] ACK 不自动修改 `AGENTS.md`、`CLAUDE.md` 或其它 Agent 指令文件。
|
||||
|
||||
## 编排
|
||||
|
||||
- [ ] 已选择 Orca 或手动模式。
|
||||
- [ ] 派发前优先复用同 worktree、同角色、同配置的空闲 worker。
|
||||
- [ ] Developer 与 Test 的启动命令通过校验。
|
||||
- [ ] 多 worktree 场景只有一个权威 `tasks.yaml`。
|
||||
- [ ] 多 worktree 场景只有一个权威 `tasks.yaml` 和 `knowledge.yaml`。
|
||||
- [ ] Test 使用的服务来自正确 worktree。
|
||||
|
||||
## 闭环
|
||||
@@ -48,4 +71,5 @@
|
||||
|
||||
首次接入建议选择一个低风险问题跑完整闭环。项目差异写回
|
||||
`docs/ack/project.md`;通用问题回流到 ACK Skill 的 `references/`、`templates/`
|
||||
或 `scripts/`,并更新 `VERSION`。
|
||||
或 `scripts/`,并更新 `VERSION`。项目特有、跨任务复用且已经验证的经验才写入
|
||||
`docs/ack/knowledge.yaml`。
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
|
||||
本文件定义**与具体编排工具无关**的三角色协作闭环。运行时调度可以用 Orca(见 `orca-adapter.md`),也可以手动跑(见下方「手动模式」)。
|
||||
|
||||
原则:调度消息只是运行时载体,**所有结论都必须回写到 `tasks.yaml`**(事实源),不要把消息当最终记录。角色定义见 `roles-and-permissions.md`:Coordinator 只编排、Test 只验证、Developer 只实现。
|
||||
原则:调度消息只是运行时载体,任务结论必须回写到 `tasks.yaml`;跨任务复用、已经
|
||||
验证的项目经验必须由 Coordinator 回写到 `knowledge.yaml`。不要把消息当最终记录。
|
||||
角色定义见 `roles-and-permissions.md`:Coordinator 只编排、Test 只验证、
|
||||
Developer 只实现。
|
||||
|
||||
---
|
||||
|
||||
@@ -12,12 +15,12 @@
|
||||
|
||||
| 抽象动作 | 含义 | Orca 实现 | 手动实现 |
|
||||
|----------|------|-----------|----------|
|
||||
| `prepare(task)` | Coordinator 把任务和验收信号写进 `tasks.yaml` | 同左 | 同左 |
|
||||
| `prepare(task)` | Coordinator 把任务和验收信号写进 `tasks.yaml`,按 scope 推荐知识并确认显式 `knowledgeRefs` | 同左 | 同左 |
|
||||
| `dispatch(task, developer)` | 把修复任务连同上下文交给 Developer | `orca orchestration dispatch` | 复制 prompt 到 Developer 终端/会话 |
|
||||
| `dispatch(task, test)` | 把复测任务连同验收信号交给 Test | `orca orchestration dispatch` | 复制 prompt 到 Test 终端/会话 |
|
||||
| `wait()` | 等待 `worker_done` / `retest_result` / `escalation` / `decision_gate` | `orca orchestration check --wait` | 人工等待回报 |
|
||||
| `gate(task)` | Coordinator 读 Test 证据并对齐原始意图(不重测) | 同左 | 同左 |
|
||||
| `writeback(task, result)` | Coordinator 把结果写回 `tasks.yaml` | 同左 | 同左 |
|
||||
| `gate(task)` | Coordinator 读 Test 证据、`knowledgeChecks` 并对齐原始意图(不重测) | 同左 | 同左 |
|
||||
| `writeback(task, result)` | Coordinator 把任务结果写回 `tasks.yaml`,把验证通过的跨任务经验按权限写入 `knowledge.yaml` | 同左 | 同左 |
|
||||
|
||||
派发用的 prompt 见 `prompt-templates.md`。状态流转见 `roles-and-permissions.md` §「任务状态机」。**独立复测由 Test 执行,不是 Coordinator**;Coordinator 只做读证据的终检。
|
||||
|
||||
@@ -28,13 +31,15 @@
|
||||
```text
|
||||
Coordinator 发现或读取 open 任务
|
||||
-> prepare:写/补全 tasks.yaml 验收信号
|
||||
-> 从 knowledge.yaml 按 scope 推荐 active 知识,Coordinator 确认固定 revision 的 knowledgeRefs
|
||||
-> 为新轮次生成稳定 attemptId(<task-id>-A<round>),Developer 与 Test 共用
|
||||
-> 决定 worktree:当前 worktree 起子 agent,还是新建隔离 worktree(见下节「子任务放哪」)
|
||||
-> 解析 worker:先复用同 worktree、同角色的空闲 worker;没有可复用项时才校验命令并新建(见 orca-adapter.md)
|
||||
-> dispatch 给 Developer(--to <worker handle>)
|
||||
-> wait:Developer 的 worker_done / escalation
|
||||
-> wait:Developer 的 worker_done / escalation(含 knowledgeApplied / knowledgeCandidates)
|
||||
-> writeback fixed_by_dev
|
||||
-> dispatch 给 Test(retesting)
|
||||
-> wait:Test 的 retest_result
|
||||
-> wait:Test 的 retest_result(含 knowledgeChecks 和 candidate 独立证据)
|
||||
-> Test 通过:gate(Coordinator 读证据对齐意图)
|
||||
-> 通过 gate:writeback verified
|
||||
-> gate 不满足意图:writeback failed_retest,带意图差异再派发 Developer
|
||||
@@ -44,6 +49,51 @@ Coordinator 发现或读取 open 任务
|
||||
|
||||
一次派发只修一个明确问题(细则见 `optimization-method.md` §「每轮派发只修一个明确问题」)。
|
||||
「决定 worktree」「解析 worker」两步的决策见下节与 `model-routing.md` / `orca-adapter.md`。
|
||||
Coordinator 默认给新逻辑轮次生成 `<task-id>-A<round>`,并在记录轮次结果时写入
|
||||
`tasks[].dispatch.rounds[].attemptId`。它独立于编排工具产生的 `taskId` 和
|
||||
`dispatchId`:前者用于知识来源追溯,后两者只用于查询运行时。0.9.0 之前的历史
|
||||
轮次允许暂时缺省;但要把该轮证据晋升为知识前,必须按确定性格式补齐。晋升时,
|
||||
`provenance.taskId` 和 `provenance.attemptId` 必须能精确回到该任务及轮次。
|
||||
|
||||
### 知识选择与晋升
|
||||
|
||||
`prepare(task)` 时可以使用 Skill 自带的 `scripts/select_knowledge.py` 按
|
||||
component、path、dependency、version 和 tag 确定性推荐条目。选择器只返回
|
||||
`active`,并受条目数限制;`stale`、`superseded` 和 `archived` 不默认派发。自动
|
||||
匹配只是推荐。条目中每个非空 scope 维度都必须被任务上下文命中,因此调用时应
|
||||
提供当前任务已知的全部 component、path、dependency、version、tag、symbol 和错误
|
||||
特征。Coordinator 确认并写入当前任务的显式 `knowledgeRefs` 后才成为本轮权威
|
||||
上下文。
|
||||
|
||||
`scope.paths` 使用 POSIX 路径 glob:`*` 不跨目录分隔符,`**` 才能跨目录。多个
|
||||
pattern 是同一维度内的 OR,候选越多表示范围越宽;超过选择预算时,选择器优先保留
|
||||
命中维度更多、OR 候选更少、字面约束更多且通配符更少的条目。
|
||||
|
||||
派发时只内联本轮引用的少量知识,不要求 worker 全量读取 `knowledge.yaml`。这也
|
||||
避免隔离 worktree 读不到 Coordinator 工作树中尚未提交的项目状态。
|
||||
|
||||
进行中的任务只能引用 `active` 条目。已经进入 `verified` 或 `leftover` 的终态任务
|
||||
可以继续保留固定 revision 的历史引用,即使对应知识后来变成 `stale`、
|
||||
`superseded` 或 `archived`;这些旧条目不会再次被选择器派发。
|
||||
|
||||
Developer 或 Test 发现经验时只能通过 `knowledgeCandidates` 提名,并带上当前任务
|
||||
的独立观测、scope、触发条件和证据。candidate 留在任务证据中,不进入知识选择器,
|
||||
也不能被下一轮当作 active 指令。只有 Test 独立验证、Coordinator gate 通过后,
|
||||
Coordinator 才能写入或更新 `knowledge.yaml`;全项目强制或权限类规则还需
|
||||
User / Decision Owner 确认。
|
||||
|
||||
Test 对显式引用回报 `not_applicable` 时,说明 Coordinator 的选择需要纠正。该引用
|
||||
仍在 `knowledgeRefs` 中就不能把任务标为 `verified`;Coordinator 应记录原因、移除
|
||||
误选引用并重新校验,或继续补充验证。
|
||||
|
||||
知识中的 `verification.ref` 只能引用
|
||||
`knowledge.yaml.verificationRegistry` 里已审查的仓库内相对 path 和结构化 args,
|
||||
不能从知识正文拼接或执行自由 shell。Developer/Test 只把 `verification.ref` 交给
|
||||
`<ack-skill-dir>/scripts/run_verification.py`;该入口从项目根目录逐段安全打开检查
|
||||
文件,拒绝 symlink 路径,并从匿名稳定快照执行。关键约束应下沉为测试、lint、CI
|
||||
或正式规范。检查进程从固定的项目根 fd cwd 运行;检查脚本应使用 cwd 或
|
||||
`ACK_PROJECT_ROOT` 定位资源。后者是 fd 路径,原始展示路径只在
|
||||
`ACK_PROJECT_ROOT_DISPLAY` 中用于日志;不能依赖 `$0` / `__file__` 的目录。
|
||||
|
||||
---
|
||||
|
||||
@@ -84,7 +134,10 @@ orca terminal create --worktree path:<new> --command "cursor-agent --yolo --mode
|
||||
| 要保持基线分支干净 | 新 worktree(feature 分支)|
|
||||
| 小改动、追求快 | 当前 worktree |
|
||||
|
||||
**任务板(SSOT)只落一处**:无论开几个 worktree,`tasks.yaml` 只认一个权威副本(通常在基线/协调所在 worktree),由 Coordinator 单写。不要每个 worktree 各留一份会分叉的任务板。模型固定方式见 `model-routing.md` 与 `orca-adapter.md`。
|
||||
**项目状态(SSOT)只落一处**:无论开几个 worktree,`tasks.yaml` 和
|
||||
`knowledge.yaml` 都只认一个权威副本(通常在基线/协调所在 worktree),由
|
||||
Coordinator 单写。不要每个 worktree 各留一份会分叉的项目状态。模型固定方式见
|
||||
`model-routing.md` 与 `orca-adapter.md`。
|
||||
|
||||
---
|
||||
|
||||
@@ -93,14 +146,17 @@ orca terminal create --worktree path:<new> --command "cursor-agent --yolo --mode
|
||||
没有编排工具时,闭环不变,只是 `dispatch` / `wait` 由人工承担:
|
||||
|
||||
1. Coordinator 在 `tasks.yaml` 写好任务和验收信号。
|
||||
2. 用 `prompt-templates.md` §1 的初始派发模板生成 prompt,手动发给 Developer(另一个会话/终端/人)。
|
||||
2. 从 `knowledge.yaml` 推荐相关 active 知识,Coordinator 确认固定 revision 的
|
||||
`knowledgeRefs`;用 `prompt-templates.md` §1 的初始派发模板生成 prompt,
|
||||
手动发给 Developer(另一个会话/终端/人)。
|
||||
3. Developer 完成后按 worker_done 模板回报。
|
||||
4. Coordinator 写回 `fixed_by_dev`,用 `prompt-templates.md` §3 的复测派发模板把任务发给 Test。
|
||||
5. Test 独立复测后按复测报告模板回报证据。
|
||||
5. Test 独立复测后按复测报告模板回报证据和 `knowledgeChecks`。
|
||||
6. Coordinator 做终检并回写 `tasks.yaml`:通过 `verified`,不过 `failed_retest`。
|
||||
7. 失败则用「复测失败再派发模板」重新发给 Developer,最多累计三轮。
|
||||
|
||||
手动模式下同样遵守:worker_done / 复测报告都不等于最终结论、只有 Coordinator 写 `tasks.yaml`、三轮失败留档。
|
||||
手动模式下同样遵守:worker_done / 复测报告都不等于最终结论、只有 Coordinator
|
||||
写 `tasks.yaml` 和 `knowledge.yaml`、三轮失败留档。
|
||||
|
||||
---
|
||||
|
||||
@@ -125,7 +181,9 @@ actual:
|
||||
snapshot evidence:
|
||||
```
|
||||
|
||||
Test 只回传证据,不写 `tasks.yaml`;由 Coordinator 落盘。
|
||||
Test 只回传证据,不写 `tasks.yaml` 或 `knowledge.yaml`;由 Coordinator 落盘。
|
||||
每条适用的显式 `knowledgeRef` 还应报告 `passed`、`failed` 或
|
||||
`not_applicable` 及证据。
|
||||
|
||||
---
|
||||
|
||||
@@ -134,6 +192,8 @@ Test 只回传证据,不写 `tasks.yaml`;由 Coordinator 落盘。
|
||||
Test 报通过后,Coordinator 不重测,而是做一次读证据的终检:
|
||||
|
||||
- Test 证据是否覆盖了任务的**每一条**验收信号(见 `optimization-method.md` §1)。
|
||||
- 当前任务显式 `knowledgeRefs` 的必需 `knowledgeChecks` 是否全部覆盖,检查引用
|
||||
是否能解析到项目已审查的入口。
|
||||
- 结果是否符合任务的**原始意图**,而不只是通过了字面文案。
|
||||
- 运行环境是否对齐(见下方「服务与 worktree 对齐」)。
|
||||
|
||||
|
||||
@@ -11,8 +11,10 @@
|
||||
2. ACK Skill 已全局安装或安装到当前项目。
|
||||
3. `skiff` 命令可用。
|
||||
|
||||
不要覆盖已有的 `docs/ack/project.md`、`docs/ack/tasks.yaml`、`AGENTS.md` 或其它
|
||||
Agent 指令文件。不要把 token、`.env` 内容或其它私有配置写入 ACK 项目状态。
|
||||
不要覆盖已有的 `docs/ack/project.md`、`docs/ack/tasks.yaml`、
|
||||
`docs/ack/knowledge.yaml`、`AGENTS.md` 或其它 Agent 指令文件。ACK 不会自动
|
||||
修改 `AGENTS.md`、`CLAUDE.md` 或其它 Agent 指令文件。不要把 token、`.env`
|
||||
内容或其它私有配置写入 ACK 项目状态。
|
||||
|
||||
## 初始化
|
||||
|
||||
@@ -33,12 +35,23 @@ skiff init ack --project <project-root>
|
||||
```text
|
||||
docs/ack/
|
||||
├── project.md
|
||||
└── tasks.yaml
|
||||
├── tasks.yaml
|
||||
└── knowledge.yaml
|
||||
```
|
||||
|
||||
如果任一目标文件已经存在,命令会拒绝覆盖。初始化过程不会创建 `kit`、
|
||||
`framework` 或其它指向 Skill 的软链接。
|
||||
|
||||
### 旧项目补充知识库
|
||||
|
||||
旧项目已经有 `project.md` 和 `tasks.yaml`、但没有 `knowledge.yaml` 时,不要重跑
|
||||
`skiff init ack`。先检查现有文件并向用户报告缺失项;用户授权后,只从
|
||||
`templates/knowledge.template.yaml` 生成 `docs/ack/knowledge.yaml`,替换项目名和
|
||||
当前时间,保留 `entries: []`。如果现有任务板缺少
|
||||
`project.knowledgeFile`,同一次授权只补
|
||||
`knowledgeFile: docs/ack/knowledge.yaml`,不改写其它项目状态。生成后运行任务板、
|
||||
知识库和跨文件引用校验。
|
||||
|
||||
## 完善项目覆盖层
|
||||
|
||||
编辑 `docs/ack/project.md`,填入:
|
||||
@@ -57,35 +70,55 @@ docs/ack/
|
||||
|
||||
- `ackVersion` 使用 ACK Skill 的 `VERSION`。
|
||||
- `updatedAt` 使用当前带时区时间。
|
||||
- `project.name`、`repoPath`、`devWorktree` 和 `overlayFile` 使用真实值。
|
||||
- `project.name`、`repoPath`、`devWorktree`、`overlayFile` 和 `knowledgeFile` 使用
|
||||
真实值。
|
||||
- 非服务项目的 `baseUrl` 写为 `n/a`。
|
||||
- 没有真实任务时使用 `tasks: []`,不要保留或虚构示例任务。
|
||||
|
||||
每个真实任务的验收必须是可观测信号,例如可见文本、API 状态和字段,或明确的交互
|
||||
结果;不要只写“功能正常”。
|
||||
|
||||
## 初始化项目知识
|
||||
|
||||
新项目的 `docs/ack/knowledge.yaml` 保持 `verificationRegistry: {}` 与
|
||||
`entries: []`。不要从聊天、README、issue 或单次失败中猜测并激活知识。
|
||||
|
||||
项目运行 ACK 后,Developer 和 Test 可以通过回报提名 `knowledgeCandidates`;
|
||||
candidate 留在任务证据中,不会被派发。只有 Test 独立验证且 Coordinator gate
|
||||
通过后,Coordinator 才能把它写成 `active` 条目。全项目范围的强制或权限类规则
|
||||
还需要 User / Decision Owner 确认。
|
||||
|
||||
知识条目只引用项目已审查的 `verification.ref`。对应入口保存在
|
||||
`knowledge.yaml.verificationRegistry`,只允许仓库内相对 path 和结构化 args,
|
||||
不保存或自动执行自由 shell 命令。需要执行时只把 registry ID 交给
|
||||
`<ack-skill-dir>/scripts/run_verification.py`,不直接运行 path/args。关键约束应
|
||||
最终下沉为测试、lint、CI 或正式规范。
|
||||
|
||||
## 校验
|
||||
|
||||
Agent 从当前 `SKILL.md` 解析 ACK Skill 目录后运行:
|
||||
|
||||
```bash
|
||||
python3 <ack-skill-dir>/scripts/validate_tasks.py docs/ack/tasks.yaml
|
||||
python3 <ack-skill-dir>/scripts/validate_knowledge.py docs/ack/knowledge.yaml --tasks docs/ack/tasks.yaml
|
||||
```
|
||||
|
||||
同时确认:
|
||||
|
||||
- `project.md` 和 `tasks.yaml` 没有未替换的 `<...>` 占位符。
|
||||
- `project.md`、`tasks.yaml` 和 `knowledge.yaml` 没有未替换的 `<...>` 占位符。
|
||||
- `project.overlayFile` 指向真实文件。
|
||||
- `project.knowledgeFile` 指向 `docs/ack/knowledge.yaml`。
|
||||
- Developer 与 Test 的验证命令可执行。
|
||||
- 网站或 API 项目写清服务启动、重启和 Base URL。
|
||||
- 任务中的固定 revision `knowledgeRefs` 都能解析,非 `active` 条目没有被派发。
|
||||
|
||||
## 初始化报告
|
||||
|
||||
完成后报告:
|
||||
|
||||
- 创建或确认的两个项目文件。
|
||||
- 创建或确认的三个项目文件。
|
||||
- 检测到的技术栈和验证命令。
|
||||
- 任务板校验结果。
|
||||
- 任务板和项目知识校验结果。
|
||||
- 仍需用户补充的值。
|
||||
|
||||
只有结构校验通过且必填项目事实完整时才称“初始化完成”;否则称“部分完成”,并列出
|
||||
|
||||
@@ -16,11 +16,15 @@
|
||||
我要做一个新需求:<一句话需求>。
|
||||
你作为 ack 的 Coordinator(PM),按 ACK Skill 的 references 规范执行:
|
||||
|
||||
1. 先读 docs/ack/project.md、references/roles-and-permissions.md、closed-loop.md、optimization-method.md。
|
||||
1. 先读 docs/ack/project.md、docs/ack/tasks.yaml,校验 docs/ack/knowledge.yaml 并
|
||||
用 `scripts/select_knowledge.py` 只读取当前任务相关的 active 条目,再读
|
||||
references/roles-and-permissions.md、closed-loop.md、optimization-method.md。
|
||||
2. 写产品文档到 docs/(PRD / 交互 / 验收),把需求拆成任务,每个任务的验收写成可观测信号(可见文本 / API 结果 / 交互结果)。
|
||||
3. 把任务写进 docs/ack/tasks.yaml(只有你写),跑 validate 校验结构。
|
||||
4. 先把「产品文档 + 任务拆分 + 验收信号」给我确认,不要急着派发。
|
||||
5. 我确认后,按 ack 闭环循环:先按 docs/ack/project.md 校验 Developer/Test worker 启动命令,
|
||||
3. 按任务 scope 从 knowledge.yaml 推荐 active 知识,确认后把固定 revision 的
|
||||
knowledgeRefs 写入任务;不要派发 candidate 或全量知识库。
|
||||
4. 把任务写进 docs/ack/tasks.yaml(只有你写),校验 tasks.yaml 和 knowledge.yaml。
|
||||
5. 先把「产品文档 + 任务拆分 + 验收信号 + 适用知识引用」给我确认,不要急着派发。
|
||||
6. 我确认后,按 ack 闭环循环:先按 docs/ack/project.md 校验 Developer/Test worker 启动命令,
|
||||
dispatch 开发 → worker_done → dispatch 测试独立复测 → 你读证据终检 → 回写 tasks.yaml;
|
||||
每个任务最多三轮,三轮不过记 leftover 并升级我复盘。
|
||||
```
|
||||
@@ -31,13 +35,17 @@
|
||||
|
||||
1. 产品文档 → `docs/PRD-<feature>.md` 等(Coordinator R/W)。
|
||||
2. 任务板 → `docs/ack/tasks.yaml`,每条任务带 `expected` + `verification`,验收写成可观测信号(见 `optimization-method.md` §1)。
|
||||
3. 校验结构:
|
||||
3. 项目知识 → 从 `docs/ack/knowledge.yaml` 按 component、path、dependency、version
|
||||
和 tag 推荐 `active` 条目,Coordinator 确认后写入固定 revision 的显式
|
||||
`knowledgeRefs`。candidate 不参与选择。
|
||||
4. 校验结构与引用:
|
||||
|
||||
```bash
|
||||
python3 <ack-skill-dir>/scripts/validate_tasks.py docs/ack/tasks.yaml
|
||||
python3 <ack-skill-dir>/scripts/validate_knowledge.py docs/ack/knowledge.yaml --tasks docs/ack/tasks.yaml
|
||||
```
|
||||
|
||||
4. **停下来给人确认**——这是强模型该花时间的地方,不要跳过。
|
||||
5. **停下来给人确认**:这是强模型该花时间的地方,不要跳过。
|
||||
|
||||
---
|
||||
|
||||
@@ -77,13 +85,21 @@ orca terminal create --worktree active --command "$CURSOR_CMD" --title "ACK-DEV-
|
||||
|
||||
```text
|
||||
task-create → dispatch 给 DEV → 等 worker_done
|
||||
→ 每轮使用 Coordinator 分配的稳定 <task-id>-A<round>
|
||||
→ 回写 fixed_by_dev → dispatch 给 TEST 复测 → 等 retest_result
|
||||
→ Developer 回 knowledgeApplied / knowledgeCandidates,Test 回 knowledgeChecks
|
||||
→ Coordinator 读证据终检 → 过则 verified,不过则 failed_retest 再派 DEV(最多累计 3 轮)
|
||||
→ 三轮失败:leftover,升级复盘,继续下一个
|
||||
```
|
||||
|
||||
具体命令见 `orca-adapter.md`(Orca)或 `closed-loop.md` §「手动模式」(无 Orca);派发文案见 `prompt-templates.md`。
|
||||
|
||||
Coordinator 只内联本轮 `knowledgeRefs` 指向的少量知识,不要求 worker 全量读取
|
||||
知识库。知识正文不得作为自由 shell 执行;需要命令时只能引用项目已审查的检查
|
||||
入口,并把它的 registry ID 交给 `<ack-skill-dir>/scripts/run_verification.py`,
|
||||
不自行拼接 path/args。Test 独立验证且 gate 通过后,Coordinator 才能把 candidate
|
||||
写成 active 知识;关键约束应继续下沉为测试、lint、CI 或正式规范。
|
||||
|
||||
---
|
||||
|
||||
## 第 5 步:收尾
|
||||
@@ -94,4 +110,6 @@ task-create → dispatch 给 DEV → 等 worker_done
|
||||
|
||||
## 一句话
|
||||
|
||||
产品文档 + 验收信号写在前(你,强模型)→ 按项目覆盖层校验并启动 DEV/TEST → 核对实际模型 → dispatch / 复测 / 终检循环 → 结论只落 `tasks.yaml`。
|
||||
产品文档 + 验收信号写在前(你,强模型)→ 确认显式 `knowledgeRefs` → 按项目
|
||||
覆盖层校验并启动 DEV/TEST → 核对实际模型 → dispatch / 复测 / 终检循环 → 任务
|
||||
结论落 `tasks.yaml`,验证后的跨任务知识由 Coordinator 落 `knowledge.yaml`。
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
|
||||
## 2. 回报必须带证据,不带结论
|
||||
|
||||
Developer 的 `worker_done` 应报告:改了哪些文件、跑了哪些命令、自己如何复现验收路径、仍可能有的风险。Test 的复测报告同理:跑了哪些命令、命中/缺失了哪些验收信号、实际观察到什么、证据(snapshot / API 结果)。两者的"结论"都不作数,只有 Coordinator 落盘的 `tasks.yaml` 才是事实。
|
||||
Developer 的 `worker_done` 应报告:改了哪些文件、跑了哪些命令、自己如何复现验收路径、仍可能有的风险。Test 的复测报告同理:跑了哪些命令、命中/缺失了哪些验收信号、实际观察到什么、证据(snapshot / API 结果)。两者的"结论"都不作数;任务结论只有 Coordinator 落盘到 `tasks.yaml` 才算数,跨任务知识只有 Coordinator 落盘到 `knowledge.yaml` 才会生效。
|
||||
|
||||
不要写:
|
||||
|
||||
@@ -138,7 +138,41 @@ one dispatch = one bug = one acceptance path
|
||||
|
||||
---
|
||||
|
||||
## 9. 结束条件
|
||||
## 9. 让经验成为有证据、会过期的知识护栏
|
||||
|
||||
项目特有、跨任务复用且会改变后续开发或验证行为的经验,可以提名到项目知识护栏。
|
||||
不要把全部对话、日志或单次猜测自动保存成“记忆”。
|
||||
|
||||
```text
|
||||
Developer / Test 当前任务观测
|
||||
-> knowledgeCandidates(只留在任务证据)
|
||||
-> Test 独立验证 + Coordinator gate
|
||||
-> Coordinator 写入 active knowledge
|
||||
-> stale
|
||||
-> superseded / archived
|
||||
```
|
||||
|
||||
`prepare(task)` 时只按 component、path、dependency、version 和 tag 推荐相关
|
||||
`active` 条目;自动匹配只负责推荐,Coordinator 确认固定 revision 的显式
|
||||
`knowledgeRefs` 才是本轮权威上下文。每轮只派发命中的少量条目,不全量注入知识库。
|
||||
|
||||
Developer 回报实际采用的 `knowledgeApplied` 和带当前观测证据的
|
||||
`knowledgeCandidates`。Test 对本轮显式引用回报 `knowledgeChecks`,并用独立观测
|
||||
确认或否定 candidate。同一 Agent 不能把自己读到的旧知识复述成新证据,避免错误
|
||||
知识自我强化。
|
||||
|
||||
知识中的验证只能引用 `knowledge.yaml.verificationRegistry` 中已经审查的仓库内
|
||||
相对 path 和结构化 args,不能把正文或选择器输出拼接成自由 shell 执行。实际运行
|
||||
只把 registry ID 交给 `<ack-skill-dir>/scripts/run_verification.py`。安全、正确性
|
||||
和兼容性等关键约束一旦稳定,应下沉为测试、lint、CI 或正式规范;知识项继续解释
|
||||
触发条件、原因与证据,不替代可执行控制。
|
||||
|
||||
依赖、配置、路径或版本变化后应重新审查相关知识。临时 workaround 必须有失效或
|
||||
移除条件;冲突规则不能靠“最后写入者获胜”处理。
|
||||
|
||||
---
|
||||
|
||||
## 10. 结束条件
|
||||
|
||||
一轮闭环结束时,必须能回答:
|
||||
|
||||
@@ -146,5 +180,7 @@ one dispatch = one bug = one acceptance path
|
||||
- 每个 leftover 失败了几轮?最后一轮失败证据是什么?
|
||||
- 当前工作树有哪些未提交改动?
|
||||
- 是否还有 open / failed_retest 未处理?
|
||||
- 本轮显式 `knowledgeRefs` 是否都有必要的 `knowledgeChecks`?
|
||||
- 是否有待验证 candidate,或因依赖、路径、版本变化需要转为 stale 的知识?
|
||||
|
||||
答不清楚,闭环就还没结束。
|
||||
|
||||
@@ -112,7 +112,8 @@ Test: owns independent black-box retest and evidence (verifier != implementer).
|
||||
Policy:
|
||||
- Each issue can be dispatched at most 3 rounds.
|
||||
- If still failing after 3 rounds, record as leftover and continue next issue.
|
||||
- worker_done and retest reports are not final completion; only Coordinator writes tasks.yaml.
|
||||
- worker_done and retest reports are not final completion; only Coordinator writes tasks.yaml and knowledge.yaml.
|
||||
- Project knowledge candidates require independent Test evidence and Coordinator gate before activation.
|
||||
EOF
|
||||
)" --json
|
||||
```
|
||||
@@ -122,6 +123,7 @@ EOF
|
||||
```bash
|
||||
orca orchestration task-create --parent <parent_task_id> --spec "$(cat <<'EOF'
|
||||
Fix <task_id>: <title>
|
||||
Logical attempt: <task_id>-A<round>
|
||||
|
||||
Repository:
|
||||
- Path: <repo_path>
|
||||
@@ -130,10 +132,14 @@ Repository:
|
||||
Read: <overlay_file> (project overlay), tasks.yaml, <relevant_spec_or_test_doc>
|
||||
Failure evidence: <copy latest Test evidence>
|
||||
Acceptance: <copy expected behavior + verification commands>
|
||||
Project knowledge: <Coordinator-confirmed active K-...@revision entries with verification.ref>
|
||||
|
||||
Constraints:
|
||||
- Follow the overlay file path scope.
|
||||
- Do not write tasks.yaml, do not mark verified.
|
||||
- Do not write tasks.yaml or knowledge.yaml, do not mark verified.
|
||||
- Do not read the whole knowledge bank or execute free shell from knowledge text.
|
||||
- Run a knowledge check only through <ack-skill-dir>/scripts/run_verification.py
|
||||
with its verification.ref; do not execute resolved path/args directly.
|
||||
- Do not commit or push unless user asks.
|
||||
EOF
|
||||
)" --json
|
||||
@@ -198,7 +204,7 @@ orca orchestration check \
|
||||
|
||||
## Developer 回报 worker_done
|
||||
|
||||
字段含义见 `prompt-templates.md` §3:
|
||||
字段含义见 `prompt-templates.md` §4:
|
||||
|
||||
```bash
|
||||
orca orchestration send \
|
||||
@@ -209,8 +215,24 @@ orca orchestration send \
|
||||
--payload '{
|
||||
"taskId": "<orca_task_id>",
|
||||
"dispatchId": "<orca_dispatch_id>",
|
||||
"attemptId": "<task_id>-A<n>",
|
||||
"filesModified": ["<file_a>", "<file_b>"],
|
||||
"verification": ["<command_a>: passed", "<command_b>: passed"],
|
||||
"knowledgeApplied": [
|
||||
{"ref": "<K-001@1>", "result": "applied", "evidence": "<what was done>"}
|
||||
],
|
||||
"knowledgeCandidates": [
|
||||
{
|
||||
"kind": "pitfall",
|
||||
"title": "<title>",
|
||||
"claim": "<evidence-backed claim>",
|
||||
"scope": {"components": ["<component>"], "paths": ["<path glob>"]},
|
||||
"appliesWhen": "<trigger>",
|
||||
"directive": "<action, not a shell command>",
|
||||
"rationale": "<why it matters>",
|
||||
"evidenceRefs": ["<current task evidence>"]
|
||||
}
|
||||
],
|
||||
"risk": "<remaining risk or none>"
|
||||
}' \
|
||||
--json
|
||||
@@ -233,11 +255,19 @@ orca orchestration send \
|
||||
--payload '{
|
||||
"taskId": "<orca_task_id>",
|
||||
"dispatchId": "<orca_dispatch_id>",
|
||||
"attemptId": "<task_id>-A<n>",
|
||||
"env": {"worktree": "<path>", "branch": "<branch>", "commit": "<sha>", "baseUrl": "<base_url>"},
|
||||
"signals": ["<signal 1>: pass", "<signal 2>: fail (<evidence>)"],
|
||||
"knowledgeChecks": [
|
||||
{"ref": "<K-001@1>", "result": "passed", "evidence": "<independent evidence>"}
|
||||
],
|
||||
"knowledgeCandidates": [],
|
||||
"conclusion": "all-signals-pass | signals-failed"
|
||||
}' \
|
||||
--json
|
||||
```
|
||||
|
||||
无 `retest_result` 类型时用 `--type worker_done`,靠 subject `retest round <n>` 区分。收到复测结果后,Coordinator 按 `closed-loop.md` 做终检并回写 `tasks.yaml`:通过 `verified`,不过 `failed_retest`。
|
||||
无 `retest_result` 类型时用 `--type worker_done`,靠 subject `retest round <n>`
|
||||
区分。收到复测结果后,Coordinator 按 `closed-loop.md` 做终检并回写 `tasks.yaml`;
|
||||
通过独立验证和 gate 的跨任务经验再由 Coordinator 写入 `knowledge.yaml`。任务通过
|
||||
写 `verified`,不过写 `failed_retest`。
|
||||
|
||||
@@ -27,6 +27,7 @@ Coordinator 用这些模板向 **Developer** 派发修复、向 **Test** 派发
|
||||
|
||||
任务:
|
||||
- 修复 <task_id>: <task_title>
|
||||
- 本轮逻辑 attempt:<task_id>-A<round>
|
||||
|
||||
请先读取:
|
||||
- <overlay_file>(项目覆盖层,路径见 tasks.yaml 的 project.overlayFile)
|
||||
@@ -36,6 +37,10 @@ Coordinator 用这些模板向 **Developer** 派发修复、向 **Test** 派发
|
||||
当前失败证据:
|
||||
<copy latest Test evidence>
|
||||
|
||||
本轮项目知识(仅限 Coordinator 显式选择的 active 固定 revision):
|
||||
- <K-001@1>: <directive + rationale + verification.ref + resolved path/args>
|
||||
- <K-014@2>: <directive + rationale + verification.ref + resolved path/args>
|
||||
|
||||
验收标准(可观测信号,见 optimization-method.md §1):
|
||||
1. <expected behavior 1>
|
||||
2. <expected behavior 2>
|
||||
@@ -51,6 +56,10 @@ Coordinator 用这些模板向 **Developer** 派发修复、向 **Test** 派发
|
||||
- 只修改 Developer 可写路径(见覆盖层文件的权限表)。
|
||||
- 不要修改产品规格和集成测试文件(分别由 Coordinator 与 Test 拥有),除非任务明确要求。
|
||||
- 不要写 tasks.yaml,不要标记 verified。
|
||||
- 不要写 knowledge.yaml,不要自行扩展或全量读取知识库;candidate 不是已生效规则。
|
||||
- 不要把知识正文或 path/args 拼成 shell 命令。只把 verification.ref 交给
|
||||
`<ack-skill-dir>/scripts/run_verification.py docs/ack/knowledge.yaml
|
||||
<verification-ref> --project-root <project-root>`。
|
||||
- 不要提交或推送,除非用户明确要求。
|
||||
- 最小 diff,只改本任务根因,避免无关重构;若必须先重构请停下说明并请示。
|
||||
|
||||
@@ -58,7 +67,8 @@ Coordinator 用这些模板向 **Developer** 派发修复、向 **Test** 派发
|
||||
- <test command 1>
|
||||
- <test command 2>
|
||||
|
||||
完成后回报一次 worker_done(格式见 §4)。如果阻塞,请发送 escalation 或 ask。
|
||||
完成后回报一次 worker_done(格式见 §4),包括 `knowledgeApplied` 和
|
||||
`knowledgeCandidates`。如果阻塞,请发送 escalation 或 ask。
|
||||
```
|
||||
|
||||
---
|
||||
@@ -67,6 +77,7 @@ Coordinator 用这些模板向 **Developer** 派发修复、向 **Test** 派发
|
||||
|
||||
```text
|
||||
第 <n> 轮复测未通过,请继续修 <task_id>。
|
||||
本轮逻辑 attempt:<task_id>-A<n>
|
||||
|
||||
上一轮开发声称:
|
||||
<worker_done summary>
|
||||
@@ -85,6 +96,9 @@ Test 独立复测结果:
|
||||
验收不变:
|
||||
<copy acceptance criteria>
|
||||
|
||||
本轮知识引用:
|
||||
<copy Coordinator confirmed knowledgeRefs; do not add candidates>
|
||||
|
||||
完成后回报 worker_done,subject 使用:"<task_id> fix ready round <n>"
|
||||
```
|
||||
|
||||
@@ -96,6 +110,7 @@ Developer 回报 worker_done 后,Coordinator 把复测任务发给 Test。
|
||||
|
||||
```text
|
||||
请对 <task_id>: <task_title> 做独立黑盒复测。
|
||||
本轮逻辑 attempt:<task_id>-A<round>
|
||||
|
||||
请先读取:
|
||||
- <overlay_file>(项目覆盖层,路径见 tasks.yaml 的 project.overlayFile)
|
||||
@@ -105,6 +120,12 @@ Developer 回报 worker_done 后,Coordinator 把复测任务发给 Test。
|
||||
Developer 本轮声称(仅供参考,不作数):
|
||||
- 改动文件:<files>
|
||||
- 自测命令:<commands>
|
||||
- 实际采用知识:<knowledgeApplied>
|
||||
- 新知识候选:<knowledgeCandidates; candidate only>
|
||||
|
||||
本轮项目知识(仅限 Coordinator 显式选择的 active 固定 revision):
|
||||
- <K-001@1>: <directive + rationale + verification.ref + resolved path/args>
|
||||
- <K-014@2>: <directive + rationale + verification.ref + resolved path/args>
|
||||
|
||||
复测要求(见 roles-and-permissions.md §三角色能力清单 · Test):
|
||||
- 先对齐运行环境(pwd / 分支 / commit / 服务 worktree,见 closed-loop.md),避免测错实例或旧构建;网站类先确认服务已按新代码重启。
|
||||
@@ -114,9 +135,14 @@ Developer 本轮声称(仅供参考,不作数):
|
||||
2. <observable signal 2>
|
||||
3. <observable signal 3>
|
||||
- 需要时把易反复误判的路径沉淀成可执行测试(见 optimization-method.md §8)。
|
||||
- 对每条适用的 `knowledgeRef`,把它的 verification.ref 交给
|
||||
`<ack-skill-dir>/scripts/run_verification.py docs/ack/knowledge.yaml
|
||||
<verification-ref> --project-root <project-root>`,并回报 `knowledgeChecks`。
|
||||
对 candidate 使用独立观测验证,不能复述 Developer 的结论作为证据。
|
||||
|
||||
约束:
|
||||
- 只读源码,不修改应用代码,不写 tasks.yaml。
|
||||
- 只读源码,不修改应用代码,不写 tasks.yaml 或 knowledge.yaml。
|
||||
- candidate 不属于 active 知识;不要全量注入知识库,不要从知识正文执行自由 shell。
|
||||
- 只回传证据和逐条结论,最终判定由 Coordinator 终检后落盘。
|
||||
|
||||
完成后回报一次复测报告(格式见 §5),subject:"<task_id> retest round <n>"。
|
||||
@@ -130,14 +156,36 @@ Developer 本轮声称(仅供参考,不作数):
|
||||
|
||||
```text
|
||||
subject: <task_id> fix ready round <n>
|
||||
attemptId: <task_id>-A<n>
|
||||
filesModified: [<file_a>, <file_b>]
|
||||
verification:
|
||||
- <command_a>: passed
|
||||
- <command_b>: passed
|
||||
reproduce: 我如何复现验收路径 <steps>
|
||||
knowledgeApplied:
|
||||
- ref: <K-001@1>
|
||||
result: <applied|not_applicable>
|
||||
evidence: <what was done or why not applicable>
|
||||
knowledgeCandidates:
|
||||
- kind: guardrail/pitfall/verification
|
||||
title: <reusable project lesson>
|
||||
claim: <evidence-backed project claim>
|
||||
scope:
|
||||
components: [<component>]
|
||||
paths: [<path glob>]
|
||||
dependencies: [<dependency>]
|
||||
versions: [<version>]
|
||||
tags: [<tag>]
|
||||
appliesWhen: <trigger>
|
||||
directive: <action, not a shell command>
|
||||
rationale: <why this changes future work>
|
||||
evidenceRefs: [<current task evidence reference>]
|
||||
risk: <remaining risk or none>
|
||||
```
|
||||
|
||||
没有命中知识或没有新 candidate 时,对应列表写 `[]`。Developer 不能把自己读到的
|
||||
旧知识复述为新 candidate;每条 candidate 都需要当前任务产生的观测证据。
|
||||
|
||||
Orca 模式下用 `orca-adapter.md` §「Developer 回报 worker_done」的命令发送同样的字段。
|
||||
|
||||
---
|
||||
@@ -148,6 +196,7 @@ Test 只回传证据和逐条结论,不下最终判定:
|
||||
|
||||
```text
|
||||
subject: <task_id> retest round <n>
|
||||
attemptId: <task_id>-A<n>
|
||||
env:
|
||||
worktree: <path>
|
||||
branch: <branch>
|
||||
@@ -159,10 +208,31 @@ signals:
|
||||
- <signal 1>: pass/fail (<evidence>)
|
||||
- <signal 2>: pass/fail (<evidence>)
|
||||
browser: <snapshot / DOM / API evidence>
|
||||
knowledgeChecks:
|
||||
- ref: <K-001@1>
|
||||
result: <passed|failed|not_applicable>
|
||||
evidence: <independent evidence>
|
||||
knowledgeCandidates:
|
||||
- kind: guardrail/pitfall/verification
|
||||
title: <new lesson found by Test>
|
||||
claim: <evidence-backed project claim>
|
||||
scope:
|
||||
components: [<component>]
|
||||
paths: [<path glob>]
|
||||
dependencies: [<dependency>]
|
||||
versions: [<version>]
|
||||
tags: [<tag>]
|
||||
appliesWhen: <trigger>
|
||||
directive: <action, not a shell command>
|
||||
rationale: <why this changes future work>
|
||||
evidenceRefs: [<independent evidence reference>]
|
||||
conclusion: all-signals-pass / signals-failed
|
||||
notes: <observations, suspected cause if failed>
|
||||
notes: <observations, suspected cause, and independent candidate validation evidence>
|
||||
```
|
||||
|
||||
没有适用项时列表写 `[]`。Test 的知识结论仍只是证据;只有 Coordinator 能写入或
|
||||
激活 `knowledge.yaml`。
|
||||
|
||||
Orca 模式下用 `orca-adapter.md` §「Test 回报复测结果」的命令发送同样的字段。
|
||||
|
||||
---
|
||||
@@ -181,6 +251,11 @@ Orca 模式下用 `orca-adapter.md` §「Test 回报复测结果」的命令发
|
||||
验证命令:
|
||||
- <command>: passed
|
||||
|
||||
项目知识:
|
||||
- 本轮采用:<knowledgeRefs and checks>
|
||||
- 新增或更新:<active/stale/superseded entries written by Coordinator, or none>
|
||||
- 待验证 candidate:<remaining candidates or none>
|
||||
|
||||
工作树状态:
|
||||
- <repo_path>: <git status summary>
|
||||
- <dev_worktree>: <git status summary>
|
||||
|
||||
@@ -12,12 +12,12 @@ ACK 默认三个独立 Agent:**Coordinator 只编排、Test 只验证、Develo
|
||||
|
||||
| 角色 | 主要职责 | 验证方式 | 不应做的事 |
|
||||
|------|----------|----------|------------|
|
||||
| Coordinator (PM) | 需求拆解、定验收信号、排优先级、写 `tasks.yaml`、向 Developer/Test 派发、跑三轮闭环、做最终 gate | 读 Test 证据并对齐原始意图(不亲自跑测试) | 修改源码、亲自复测、凭 worker_done 直接标 `verified` |
|
||||
| Test | 黑盒复测、回归验证、沉淀可执行测试、产出证据 | 浏览器、API、集成脚本、用户可见行为 | 修改应用源码、修改产品规格、写 `tasks.yaml` |
|
||||
| Developer | 实现修复、写单元测试、运行构建和白盒验证 | 单元测试、类型检查、构建、本地运行 | 修改产品规格与集成测试、标记 `verified`、绕过测试声称完成 |
|
||||
| Coordinator (PM) | 需求拆解、定验收信号、排优先级、单写 `tasks.yaml` / `knowledge.yaml`、选择知识、向 Developer/Test 派发、跑三轮闭环、做最终 gate | 读 Test 证据并对齐原始意图(不亲自跑测试) | 修改源码、亲自复测、凭 worker_done 直接标 `verified`、自动激活未验证知识 |
|
||||
| Test | 黑盒复测、回归验证、执行知识检查、独立验证知识候选、沉淀可执行测试、产出证据 | 浏览器、API、集成脚本、用户可见行为 | 修改应用源码、修改产品规格、写 `tasks.yaml` 或 `knowledge.yaml` |
|
||||
| Developer | 实现修复、写单元测试、运行构建和白盒验证、提名项目知识 | 单元测试、类型检查、构建、本地运行 | 修改产品规格与集成测试、写项目状态、标记 `verified`、绕过测试声称完成 |
|
||||
| User / Decision Owner | 决定范围、优先级、阻塞项是否继续 | 审阅报告和遗留清单 | 直接替代复测证据 |
|
||||
|
||||
**独立验证权归 Test。** Coordinator 不亲自复测——它读 Test 的证据,并对照任务的原始意图做一次终检(见「完成定义」)。`worker_done` 不等于完成的原则同时适用于 Developer 和 Test:结论只有落到 `tasks.yaml` 才算数。
|
||||
**独立验证权归 Test。** Coordinator 不亲自复测,它读 Test 的证据,并对照任务的原始意图做一次终检(见「完成定义」)。`worker_done` 不等于完成的原则同时适用于 Developer 和 Test:结论只有落到 `tasks.yaml` 才算数。
|
||||
|
||||
**模型档位(正交层)。** 三角色默认按成本分层:Coordinator 用强模型,Test 与 Developer 用中低模型,必要时升级。完整档位表与升级规则见 `model-routing.md`。Coordinator 用强模型但不跑测试,这一分工天然省 token 又不破坏「验证者 ≠ 实现者」。
|
||||
|
||||
@@ -29,7 +29,7 @@ ACK 默认三个独立 Agent:**Coordinator 只编排、Test 只验证、Develo
|
||||
|
||||
这些是**通用工程习惯**,不含项目命令与路径;项目差异写在覆盖层文件(默认 `docs/ack/project.md`)。装了外部 skill 的环境可按每个角色末尾的「可选 skills」加速,未装则照本清单执行,不阻塞。
|
||||
|
||||
### Coordinator (PM) —— 拆解与终检
|
||||
### Coordinator (PM):拆解与终检
|
||||
|
||||
- **Outcome**:把一句话需求变成可执行、验收可观测的任务集,并跑完闭环得到明确结论(verified / leftover)。
|
||||
- **Must Do**
|
||||
@@ -37,38 +37,58 @@ ACK 默认三个独立 Agent:**Coordinator 只编排、Test 只验证、Develo
|
||||
- 每个任务写**可观测验收信号**(可见文本 / API 结果 / 交互结果,见 `optimization-method.md` §1),而不是「功能正常」。
|
||||
- 拆任务时点明最脆弱的假设:「本任务假设 X,若 X 不成立则 Y」;列出被否掉的方案与原因。
|
||||
- 拆分/验收先给用户确认,再派发(`kickoff.md` 第 1 步的停顿点)。
|
||||
- `prepare(task)` 时按 component、path、dependency、version 和 tag 从
|
||||
`knowledge.yaml` 推荐相关 `active` 知识;人工确认后把固定 revision 的
|
||||
`knowledgeRefs` 写入当前任务上下文。
|
||||
- 新逻辑轮次默认分配稳定的 `<task-id>-A<round>`,记录到
|
||||
`dispatch.rounds[].attemptId`;旧轮次作为知识来源前再补齐,不要用编排工具的
|
||||
`dispatchId` 代替。
|
||||
- gate 时检查 Developer 的 `knowledgeApplied`、Test 的 `knowledgeChecks` 和
|
||||
candidate 独立证据;只有证据充分时才由 Coordinator 激活、废弃或替代知识。
|
||||
- 一次派发只针对一个明确问题(`optimization-method.md` §6);每任务最多三轮。
|
||||
- 终检:读 Test 证据,逐条对齐原始意图后才落 `verified`,不亲自复测。
|
||||
- **Must Not**:改源码、亲自跑测试、凭 `worker_done` 直接标 `verified`、把多个无关失败塞进一次派发。
|
||||
- **Must Not**:改源码、亲自跑测试、凭 `worker_done` 直接标 `verified`、把多个无关失败塞进一次派发、派发 `candidate` 或全量注入知识库、把知识正文当作 shell 执行。
|
||||
- **Evidence**:产品文档、`tasks.yaml` 里的 `expected` + `verification`、Test 回传的复测证据。
|
||||
- **Output**:确认前给「产品文档 + 任务拆分 + 验收信号」;闭环结束给最终报告(`prompt-templates.md` §6)。
|
||||
- **可选 skills**:复杂需求可先用 `/think` 或 `superpowers:brainstorming` / `writing-plans` 收敛设计与计划。
|
||||
|
||||
### Developer —— 实现与白盒验证
|
||||
### Developer:实现与白盒验证
|
||||
|
||||
- **Outcome**:在授权路径内做出满足验收信号的最小改动,并用白盒证据证明它可复现。
|
||||
- **Must Do**
|
||||
- 动手前先读覆盖层文件、`tasks.yaml` 对应任务、相关规格;复现失败现象或先写会失败的测试。
|
||||
- 只使用 Coordinator 本轮显式派发的 `active` 知识,按固定 revision 回报
|
||||
`knowledgeApplied`;发现跨任务可复用的项目经验时提交带当前观测证据的
|
||||
`knowledgeCandidates`。
|
||||
- 最小 diff,只改一个明确问题的根因,不顺手重构无关代码。
|
||||
- 行为变更配单元测试;bug 修复先有一个能复现的失败用例再修。
|
||||
- 完成前跑覆盖层里规定的命令(构建 / 单测 / 本地运行),亲自走一遍验收路径。
|
||||
- **网站 / 常驻服务**:改完重启服务(或触发热更并确认生效),保证运行实例跑的是新代码,避免 Test 测到旧进程 / 旧构建。
|
||||
- **Must Not**:改产品规格与集成测试、写 `tasks.yaml`、标 `verified`、绕过测试声称完成、把 bug 修复扩成大重构(需要就先停下说明并请示)。
|
||||
- **Evidence**:改了哪些文件、跑了哪些命令及结果、如何复现验收路径、残留风险。
|
||||
- **Must Not**:改产品规格与集成测试、写 `tasks.yaml` 或 `knowledge.yaml`、标
|
||||
`verified`、把 candidate 当作已生效规则、绕过测试声称完成、把 bug 修复扩成大
|
||||
重构(需要就先停下说明并请示)。
|
||||
- **Evidence**:改了哪些文件、跑了哪些命令及结果、如何复现验收路径、实际采用的
|
||||
`knowledgeRefs`、新 candidate 的当前任务证据、残留风险。
|
||||
- **Output**:一次 `worker_done`,字段见 `prompt-templates.md` §4(只报证据,不下最终结论)。
|
||||
- **可选 skills**:排查用 `/hunt` 或 `superpowers:systematic-debugging`(先根因后修);实现行为变更用 `superpowers:test-driven-development`。
|
||||
|
||||
### Test —— 独立黑盒复测
|
||||
### Test:独立黑盒复测
|
||||
|
||||
- **Outcome**:以独立视角复现验收路径,逐条给出通过/失败的可观测证据,供 Coordinator 终检。
|
||||
- **Must Do**
|
||||
- 先对齐运行环境(pwd / 分支 / commit / 服务 worktree,见 `closed-loop.md`),避免测错实例或旧构建;网站类先确认服务已按新代码重启。
|
||||
- 逐条验证验收信号,验证交互后的真实状态,而不是只看静态文案。
|
||||
- 对 Coordinator 派发的每条 `knowledgeRef` 执行适用的额外检查,回报
|
||||
`knowledgeChecks`;对本轮 candidate 使用独立观测验证,不能复述 Developer
|
||||
结论当作证据。
|
||||
- **网站类任务优先用浏览器复测**真实交互(点击 / 跳转 / 渲染),其次才是 API / 脚本;纯后端 / CLI 则以 API smoke 或脚本为主。
|
||||
- 把最容易反复误判的路径沉淀成可执行测试(`optimization-method.md` §8)。
|
||||
- 只回传证据 + 逐条结论,最终判定留给 Coordinator。
|
||||
- **Must Not**:改应用源码、改产品规格、写 `tasks.yaml`、凭 Developer 的 `worker_done` 直接下结论。
|
||||
- **Evidence**:运行环境快照、命令结果、每条信号 pass/fail + 证据(snapshot / DOM / API 结果)。
|
||||
- **Must Not**:改应用源码、改产品规格、写 `tasks.yaml` 或 `knowledge.yaml`、凭
|
||||
Developer 的 `worker_done` 直接下结论、把 candidate 作为 active 知识执行。
|
||||
- **Evidence**:运行环境快照、命令结果、每条信号 pass/fail + 证据
|
||||
(snapshot / DOM / API 结果)、每条适用知识的
|
||||
`passed` / `failed` / `not_applicable` + 证据。
|
||||
- **Output**:一次复测报告,字段见 `prompt-templates.md` §5。
|
||||
- **可选 skills**:合并 / 发版前检查可用 `/check` 或 `superpowers:verification-before-completion`(证据先于结论)。
|
||||
|
||||
@@ -87,7 +107,8 @@ ACK 默认三个独立 Agent:**Coordinator 只编排、Test 只验证、Develo
|
||||
| `<unit_test_paths>` | Read-only | Read-only | R/W | 单元测试 |
|
||||
| `<shared_config_templates>` | Read-only | Read-only | R/W | 可提交配置模板 |
|
||||
| `<local_config>` | Read-only | Read-only | Read-only | 本地私有配置,不提交 |
|
||||
| `tasks.yaml` | R/W | Read-only | Read-only | 见下方「任务板写入约定」 |
|
||||
| `tasks.yaml` | R/W | Read-only | Read-only | 见下方「项目状态写入约定」 |
|
||||
| `knowledge.yaml` | R/W | Read-only | Read-only | Coordinator 单写;Developer/Test 通过回报提名或验证 |
|
||||
|
||||
---
|
||||
|
||||
@@ -126,15 +147,24 @@ failed_retest(累计 3 轮) -> leftover
|
||||
|
||||
---
|
||||
|
||||
## 任务板写入约定(并发安全)
|
||||
## 项目状态写入约定(并发安全)
|
||||
|
||||
`tasks.yaml` 是持久事实源,为避免多 Agent 并发写冲突:
|
||||
`tasks.yaml` 是任务事实源,`knowledge.yaml` 是跨任务项目知识事实源。为避免多
|
||||
Agent 并发写冲突:
|
||||
|
||||
- **只有 Coordinator 写 `tasks.yaml`**。Test 与 Developer 对它都是只读的。
|
||||
- **只有 Coordinator 写 `tasks.yaml` 和 `knowledge.yaml`**。Test 与 Developer
|
||||
对它们都是只读的。
|
||||
- Developer 的实现状态、Test 的复测证据都通过消息回传(`worker_done` / 复测报告),由 Coordinator 落盘。
|
||||
- Developer 和 Test 只能通过 `knowledgeCandidates` 提名知识;candidate 保存在
|
||||
当前任务证据中,在 Test 独立验证和 Coordinator gate 前不写成可派发的 active
|
||||
知识。
|
||||
- 每次写入前先读最新内容,写入后更新顶层 `updatedAt`。
|
||||
- 单次写入应是一个任务的一次状态跃迁,避免整表批量重写。
|
||||
|
||||
全项目范围的 `must`、`never` 或权限类规则还需要 User / Decision Owner 确认。
|
||||
关键约束应最终下沉为测试、lint、CI 或正式规范;知识条目保存触发条件、原因和
|
||||
证据引用,不替代可执行控制。
|
||||
|
||||
---
|
||||
|
||||
## 完成定义(Definition of Done)
|
||||
@@ -144,6 +174,8 @@ failed_retest(累计 3 轮) -> leftover
|
||||
- Developer 已提供修改文件和白盒验证证据(`worker_done`)。
|
||||
- Test 在正确 worktree 和正确服务实例上独立复测通过(对齐检查见 `closed-loop.md`),并产出可观测证据。
|
||||
- 相关单元测试、构建、集成或浏览器检查通过。
|
||||
- 当前任务显式 `knowledgeRefs` 对应的必需 `knowledgeChecks` 已由 Test 覆盖;
|
||||
未覆盖或检查引用无法解析时不能标记 `verified`。
|
||||
- **Coordinator 终检**:读 Test 的证据,确认它满足任务的原始意图与验收信号(不是重测,是审证据 + 对齐意图;避免"过了字面没过意图")。
|
||||
- `tasks.yaml` 中记录了复测证据与 `resolution.verifiedBy`。
|
||||
- 用户可见行为符合验收标准。
|
||||
|
||||
Executable
+430
@@ -0,0 +1,430 @@
|
||||
#!/usr/bin/env python3
|
||||
"""安全执行 knowledge.yaml 中已审查的 verificationRegistry 条目。
|
||||
|
||||
本入口只接受 registry ID,不接受额外命令或参数。执行前只打开一次项目根目录
|
||||
fd,知识库、检查目标和子进程 cwd 都固定到该 fd;检查文件逐段以 O_NOFOLLOW
|
||||
打开后复制到匿名、尽可能 sealed 的稳定快照,再使用结构化 argv 和 shell=False
|
||||
启动,避免检查与执行之间被替换。
|
||||
|
||||
退出码: 0..125 沿用检查结果 / 1 知识或执行失败 / 2 环境、路径或用法错误。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import fcntl
|
||||
import os
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path, PurePosixPath
|
||||
from typing import Any
|
||||
|
||||
from validate_knowledge import ( # type: ignore
|
||||
infer_project_root,
|
||||
load_yaml_text,
|
||||
validate_all,
|
||||
)
|
||||
|
||||
MAX_KNOWLEDGE_BYTES = 16 * 1024 * 1024
|
||||
MAX_TARGET_BYTES = 16 * 1024 * 1024
|
||||
|
||||
|
||||
def _project_root(
|
||||
value: str | None,
|
||||
knowledge_path: Path,
|
||||
) -> tuple[Path | None, str | None]:
|
||||
inferred = infer_project_root(knowledge_path)
|
||||
if value is not None:
|
||||
candidate = Path(value).expanduser()
|
||||
if not candidate.is_dir():
|
||||
return None, f"项目根目录不存在: {candidate}"
|
||||
resolved = candidate.resolve(strict=True)
|
||||
if inferred is not None and resolved != inferred:
|
||||
return (
|
||||
None,
|
||||
f"--project-root {resolved} 与 knowledge.yaml 推断的项目根目录 "
|
||||
f"{inferred} 不一致",
|
||||
)
|
||||
return resolved, None
|
||||
if inferred is None:
|
||||
return None, "无法从 knowledge.yaml 确定现有项目根目录"
|
||||
return inferred, None
|
||||
|
||||
|
||||
def _validate_knowledge_location(
|
||||
knowledge_path: Path,
|
||||
project_root: Path,
|
||||
) -> str | None:
|
||||
expected = project_root / "docs" / "ack" / "knowledge.yaml"
|
||||
lexical = Path(os.path.abspath(knowledge_path.expanduser()))
|
||||
if lexical != expected:
|
||||
return (
|
||||
"只允许执行项目权威知识库 "
|
||||
f"{expected},当前输入为 {lexical}"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def _open_regular_beneath(
|
||||
project_root: Path | int,
|
||||
relative_path: str,
|
||||
*,
|
||||
require_executable: bool,
|
||||
) -> tuple[int | None, str | None]:
|
||||
"""从根目录 fd 逐段打开目标,不允许任一段通过 symlink 跳转。"""
|
||||
if not hasattr(os, "O_NOFOLLOW") or not hasattr(os, "O_DIRECTORY"):
|
||||
return None, "当前平台不支持安全的 O_NOFOLLOW/O_DIRECTORY 路径解析"
|
||||
parts = PurePosixPath(relative_path).parts
|
||||
if (
|
||||
not parts
|
||||
or PurePosixPath(relative_path).is_absolute()
|
||||
or any(part in {"", ".", ".."} for part in parts)
|
||||
):
|
||||
return None, "检查目标必须是规范的项目内相对路径"
|
||||
|
||||
directory_fds: list[int] = []
|
||||
target_fd: int | None = None
|
||||
try:
|
||||
root_fd = (
|
||||
os.dup(project_root)
|
||||
if isinstance(project_root, int)
|
||||
else os.open(
|
||||
project_root,
|
||||
os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW,
|
||||
)
|
||||
)
|
||||
directory_fds.append(root_fd)
|
||||
current_fd = root_fd
|
||||
for segment in parts[:-1]:
|
||||
current_fd = os.open(
|
||||
segment,
|
||||
os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW,
|
||||
dir_fd=current_fd,
|
||||
)
|
||||
directory_fds.append(current_fd)
|
||||
target_fd = os.open(
|
||||
parts[-1],
|
||||
os.O_RDONLY | os.O_NOFOLLOW,
|
||||
dir_fd=current_fd,
|
||||
)
|
||||
metadata = os.fstat(target_fd)
|
||||
if not stat.S_ISREG(metadata.st_mode):
|
||||
os.close(target_fd)
|
||||
return None, "检查目标不是普通文件"
|
||||
if require_executable and metadata.st_mode & 0o111 == 0:
|
||||
os.close(target_fd)
|
||||
return None, "检查目标不可执行"
|
||||
return target_fd, None
|
||||
except OSError as exc:
|
||||
if target_fd is not None:
|
||||
os.close(target_fd)
|
||||
return None, f"检查目标不存在、不可访问或路径包含软链接: {exc}"
|
||||
finally:
|
||||
for directory_fd in reversed(directory_fds):
|
||||
os.close(directory_fd)
|
||||
|
||||
|
||||
def _stable_metadata(before: os.stat_result, after: os.stat_result) -> bool:
|
||||
fields = (
|
||||
"st_dev",
|
||||
"st_ino",
|
||||
"st_mode",
|
||||
"st_size",
|
||||
"st_mtime_ns",
|
||||
"st_ctime_ns",
|
||||
)
|
||||
return all(getattr(before, field) == getattr(after, field) for field in fields)
|
||||
|
||||
|
||||
def _read_stable_bytes(
|
||||
source_fd: int,
|
||||
*,
|
||||
maximum: int,
|
||||
) -> tuple[bytes | None, str | None]:
|
||||
before = os.fstat(source_fd)
|
||||
if before.st_size > maximum:
|
||||
return None, f"知识库超过大小上限 {maximum} bytes"
|
||||
chunks: list[bytes] = []
|
||||
total = 0
|
||||
try:
|
||||
os.lseek(source_fd, 0, os.SEEK_SET)
|
||||
while True:
|
||||
chunk = os.read(source_fd, min(1024 * 1024, maximum - total + 1))
|
||||
if not chunk:
|
||||
break
|
||||
chunks.append(chunk)
|
||||
total += len(chunk)
|
||||
if total > maximum:
|
||||
return None, f"知识库超过大小上限 {maximum} bytes"
|
||||
except OSError as exc:
|
||||
return None, f"无法读取权威知识库快照: {exc}"
|
||||
after = os.fstat(source_fd)
|
||||
if not _stable_metadata(before, after):
|
||||
return None, "权威知识库在读取期间发生变化,拒绝执行"
|
||||
return b"".join(chunks), None
|
||||
|
||||
|
||||
def load_authoritative_knowledge(
|
||||
project_root: Path | int,
|
||||
) -> tuple[dict[str, Any] | None, str | None]:
|
||||
source_fd, open_error = _open_regular_beneath(
|
||||
project_root,
|
||||
"docs/ack/knowledge.yaml",
|
||||
require_executable=False,
|
||||
)
|
||||
if open_error is not None or source_fd is None:
|
||||
return None, open_error or "无法安全打开权威知识库"
|
||||
try:
|
||||
content, read_error = _read_stable_bytes(
|
||||
source_fd,
|
||||
maximum=MAX_KNOWLEDGE_BYTES,
|
||||
)
|
||||
finally:
|
||||
os.close(source_fd)
|
||||
if read_error is not None or content is None:
|
||||
return None, read_error or "无法读取权威知识库"
|
||||
try:
|
||||
text = content.decode("utf-8")
|
||||
except UnicodeDecodeError as exc:
|
||||
return None, f"权威知识库不是有效 UTF-8: {exc}"
|
||||
return load_yaml_text(text, "知识库"), None
|
||||
|
||||
|
||||
def _write_all(file_descriptor: int, chunk: bytes) -> None:
|
||||
remaining = memoryview(chunk)
|
||||
while remaining:
|
||||
written = os.write(file_descriptor, remaining)
|
||||
if written <= 0:
|
||||
raise OSError("无法写入检查快照")
|
||||
remaining = remaining[written:]
|
||||
|
||||
|
||||
def _snapshot_executable(source_fd: int) -> tuple[int | None, str | None]:
|
||||
"""复制到匿名快照;Linux 上进一步 seal,冻结本次执行内容。"""
|
||||
before = os.fstat(source_fd)
|
||||
if before.st_size > MAX_TARGET_BYTES:
|
||||
return None, f"检查目标超过大小上限 {MAX_TARGET_BYTES} bytes"
|
||||
snapshot_fd: int | None = None
|
||||
seal_snapshot = all(
|
||||
hasattr(owner, name)
|
||||
for owner, name in (
|
||||
(os, "memfd_create"),
|
||||
(os, "MFD_ALLOW_SEALING"),
|
||||
(os, "MFD_CLOEXEC"),
|
||||
(fcntl, "F_ADD_SEALS"),
|
||||
(fcntl, "F_SEAL_SEAL"),
|
||||
(fcntl, "F_SEAL_SHRINK"),
|
||||
(fcntl, "F_SEAL_GROW"),
|
||||
(fcntl, "F_SEAL_WRITE"),
|
||||
)
|
||||
)
|
||||
try:
|
||||
if seal_snapshot:
|
||||
snapshot_fd = os.memfd_create( # type: ignore[attr-defined]
|
||||
"ack-verification",
|
||||
os.MFD_CLOEXEC | os.MFD_ALLOW_SEALING, # type: ignore[attr-defined]
|
||||
)
|
||||
else:
|
||||
snapshot_fd, snapshot_path = tempfile.mkstemp(
|
||||
prefix="ack-verification-"
|
||||
)
|
||||
os.unlink(snapshot_path)
|
||||
|
||||
os.lseek(source_fd, 0, os.SEEK_SET)
|
||||
total = 0
|
||||
while True:
|
||||
chunk = os.read(
|
||||
source_fd,
|
||||
min(1024 * 1024, MAX_TARGET_BYTES - total + 1),
|
||||
)
|
||||
if not chunk:
|
||||
break
|
||||
_write_all(snapshot_fd, chunk)
|
||||
total += len(chunk)
|
||||
if total > MAX_TARGET_BYTES:
|
||||
os.close(snapshot_fd)
|
||||
return None, f"检查目标超过大小上限 {MAX_TARGET_BYTES} bytes"
|
||||
after = os.fstat(source_fd)
|
||||
if not _stable_metadata(before, after):
|
||||
os.close(snapshot_fd)
|
||||
return None, "检查目标在创建执行快照期间发生变化,拒绝执行"
|
||||
|
||||
os.fchmod(snapshot_fd, before.st_mode & 0o777)
|
||||
os.lseek(snapshot_fd, 0, os.SEEK_SET)
|
||||
if seal_snapshot:
|
||||
seals = (
|
||||
fcntl.F_SEAL_SEAL
|
||||
| fcntl.F_SEAL_SHRINK
|
||||
| fcntl.F_SEAL_GROW
|
||||
| fcntl.F_SEAL_WRITE
|
||||
)
|
||||
fcntl.fcntl(snapshot_fd, fcntl.F_ADD_SEALS, seals)
|
||||
return snapshot_fd, None
|
||||
except OSError as exc:
|
||||
if snapshot_fd is not None:
|
||||
os.close(snapshot_fd)
|
||||
return None, f"无法创建稳定的检查执行快照: {exc}"
|
||||
|
||||
|
||||
def open_target(
|
||||
data: dict[str, Any],
|
||||
verification_ref: str,
|
||||
project_root: Path | int,
|
||||
) -> tuple[int | None, list[str] | None, str | None]:
|
||||
registry = data.get("verificationRegistry")
|
||||
if not isinstance(registry, dict) or verification_ref not in registry:
|
||||
return None, None, f"verification.ref {verification_ref!r} 未在 registry 注册"
|
||||
target = registry.get(verification_ref)
|
||||
if not isinstance(target, dict):
|
||||
return None, None, f"verificationRegistry.{verification_ref} 不是对象"
|
||||
relative_path = target.get("path")
|
||||
args = target.get("args")
|
||||
if not isinstance(relative_path, str) or not isinstance(args, list):
|
||||
return None, None, f"verificationRegistry.{verification_ref} 结构无效"
|
||||
if any(not isinstance(arg, str) for arg in args):
|
||||
return (
|
||||
None,
|
||||
None,
|
||||
f"verificationRegistry.{verification_ref}.args 必须是字符串数组",
|
||||
)
|
||||
source_fd, error = _open_regular_beneath(
|
||||
project_root,
|
||||
relative_path,
|
||||
require_executable=True,
|
||||
)
|
||||
if error is not None or source_fd is None:
|
||||
return None, None, error or "无法安全打开检查目标"
|
||||
try:
|
||||
target_fd, snapshot_error = _snapshot_executable(source_fd)
|
||||
finally:
|
||||
os.close(source_fd)
|
||||
if snapshot_error is not None or target_fd is None:
|
||||
return None, None, snapshot_error or "无法创建检查执行快照"
|
||||
return target_fd, args, None
|
||||
|
||||
|
||||
def _fd_executable_path(target_fd: int) -> str | None:
|
||||
for prefix in ("/proc/self/fd", "/dev/fd"):
|
||||
candidate = f"{prefix}/{target_fd}"
|
||||
if Path(candidate).exists():
|
||||
return candidate
|
||||
return None
|
||||
|
||||
|
||||
def _fd_directory_path(directory_fd: int) -> str | None:
|
||||
for prefix in ("/proc/self/fd", "/dev/fd"):
|
||||
candidate = f"{prefix}/{directory_fd}"
|
||||
if Path(candidate).is_dir():
|
||||
return candidate
|
||||
return None
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="执行 ACK verificationRegistry 中已审查的检查"
|
||||
)
|
||||
parser.add_argument("knowledge", help="knowledge.yaml 路径")
|
||||
parser.add_argument("verification_ref", help="verificationRegistry 中的检查 ID")
|
||||
parser.add_argument(
|
||||
"--project-root",
|
||||
help="项目根目录;默认从 knowledge.yaml 的 docs/ack 布局或 Git 推断",
|
||||
)
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
knowledge_path = Path(args.knowledge)
|
||||
project_root, root_error = _project_root(args.project_root, knowledge_path)
|
||||
if project_root is None:
|
||||
sys.stderr.write(f"{root_error or '无法确定现有项目根目录'}\n")
|
||||
return 2
|
||||
location_error = _validate_knowledge_location(knowledge_path, project_root)
|
||||
if location_error is not None:
|
||||
sys.stderr.write(f"{location_error}\n")
|
||||
return 2
|
||||
|
||||
try:
|
||||
project_root_fd = os.open(
|
||||
project_root,
|
||||
os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW,
|
||||
)
|
||||
except OSError as exc:
|
||||
sys.stderr.write(f"无法安全打开项目根目录: {exc}\n")
|
||||
return 2
|
||||
|
||||
try:
|
||||
stable_root_path = _fd_directory_path(project_root_fd)
|
||||
if stable_root_path is None:
|
||||
sys.stderr.write("当前平台无法固定项目根目录文件描述符\n")
|
||||
return 2
|
||||
data, knowledge_error = load_authoritative_knowledge(project_root_fd)
|
||||
if knowledge_error is not None or data is None:
|
||||
sys.stderr.write(f"{knowledge_error or '无法读取权威知识库'}\n")
|
||||
return 2
|
||||
schema_path = (
|
||||
Path(__file__).resolve().parent.parent
|
||||
/ "templates"
|
||||
/ "knowledge.schema.json"
|
||||
)
|
||||
errors, mode = validate_all(
|
||||
data,
|
||||
schema_path,
|
||||
project_root=Path(stable_root_path),
|
||||
)
|
||||
if errors:
|
||||
sys.stderr.write(f"知识库校验失败({mode}),拒绝执行:\n")
|
||||
for error in errors:
|
||||
sys.stderr.write(f" - {error}\n")
|
||||
return 1
|
||||
|
||||
target_fd, target_args, error = open_target(
|
||||
data,
|
||||
args.verification_ref,
|
||||
project_root_fd,
|
||||
)
|
||||
if error is not None or target_fd is None or target_args is None:
|
||||
sys.stderr.write(f"{error or '无法解析检查目标'}\n")
|
||||
return 2
|
||||
|
||||
try:
|
||||
executable_path = _fd_executable_path(target_fd)
|
||||
if executable_path is None:
|
||||
sys.stderr.write(
|
||||
"当前平台无法从已打开的文件描述符安全执行检查\n"
|
||||
)
|
||||
return 2
|
||||
environment = os.environ.copy()
|
||||
environment["ACK_PROJECT_ROOT"] = stable_root_path
|
||||
environment["ACK_PROJECT_ROOT_DISPLAY"] = str(project_root)
|
||||
environment["ACK_VERIFICATION_REF"] = args.verification_ref
|
||||
registry = data.get("verificationRegistry")
|
||||
target = (
|
||||
registry.get(args.verification_ref)
|
||||
if isinstance(registry, dict)
|
||||
else None
|
||||
)
|
||||
if isinstance(target, dict) and isinstance(target.get("path"), str):
|
||||
environment["ACK_VERIFICATION_PATH"] = target["path"]
|
||||
try:
|
||||
completed = subprocess.run(
|
||||
[executable_path, *target_args],
|
||||
cwd=stable_root_path,
|
||||
env=environment,
|
||||
shell=False,
|
||||
check=False,
|
||||
pass_fds=(project_root_fd, target_fd),
|
||||
)
|
||||
except OSError as exc:
|
||||
sys.stderr.write(f"检查启动失败: {exc}\n")
|
||||
return 1
|
||||
finally:
|
||||
os.close(target_fd)
|
||||
finally:
|
||||
os.close(project_root_fd)
|
||||
if 0 <= completed.returncode <= 125:
|
||||
return completed.returncode
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Executable
+275
@@ -0,0 +1,275 @@
|
||||
#!/usr/bin/env python3
|
||||
"""按显式项目上下文确定性选择 active ACK 知识。
|
||||
|
||||
匹配采用大小写敏感 glob;entry 中每个非空 scope 维度都必须被查询上下文命中。
|
||||
scope.all=true 的条目始终命中并优先占用 --limit,数量超过预算时显式失败。其余
|
||||
结果按作用域具体程度及稳定引用排序。本脚本只输出数据,绝不执行 knowledge.yaml
|
||||
中的任何文本。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import fnmatch
|
||||
import json
|
||||
import sys
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from validate_knowledge import ( # type: ignore
|
||||
SCOPE_FIELDS,
|
||||
infer_project_root,
|
||||
load_yaml,
|
||||
stable_ref,
|
||||
validate_builtin_structure,
|
||||
validate_semantics,
|
||||
)
|
||||
|
||||
DEFAULT_LIMIT = 10
|
||||
MAX_LIMIT = 100
|
||||
|
||||
|
||||
def _path_glob_matches(value: str, pattern: str) -> bool:
|
||||
"""路径 glob:* 只匹配单段,只有完整的 ** 段可以跨越 /。"""
|
||||
value_parts = tuple(value.split("/"))
|
||||
pattern_parts = tuple(pattern.split("/"))
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def match(pattern_index: int, value_index: int) -> bool:
|
||||
if pattern_index == len(pattern_parts):
|
||||
return value_index == len(value_parts)
|
||||
pattern_part = pattern_parts[pattern_index]
|
||||
if pattern_part == "**":
|
||||
return match(pattern_index + 1, value_index) or (
|
||||
value_index < len(value_parts)
|
||||
and match(pattern_index, value_index + 1)
|
||||
)
|
||||
return (
|
||||
value_index < len(value_parts)
|
||||
and fnmatch.fnmatchcase(value_parts[value_index], pattern_part)
|
||||
and match(pattern_index + 1, value_index + 1)
|
||||
)
|
||||
|
||||
return match(0, 0)
|
||||
|
||||
|
||||
def scope_matches(
|
||||
scope: dict[str, Any], context: dict[str, list[str]]
|
||||
) -> bool:
|
||||
if scope.get("all") is True:
|
||||
return True
|
||||
constrained = False
|
||||
for field in SCOPE_FIELDS:
|
||||
patterns = scope.get(field)
|
||||
if not isinstance(patterns, list) or not patterns:
|
||||
continue
|
||||
constrained = True
|
||||
values = context.get(field, [])
|
||||
matcher = _path_glob_matches if field == "paths" else fnmatch.fnmatchcase
|
||||
if not values or not any(
|
||||
matcher(value, pattern)
|
||||
for pattern in patterns
|
||||
if isinstance(pattern, str)
|
||||
for value in values
|
||||
):
|
||||
return False
|
||||
return constrained
|
||||
|
||||
|
||||
def _pattern_specificity(pattern: str) -> tuple[int, int, int, int, int]:
|
||||
wildcard_count = sum(pattern.count(char) for char in ("*", "?", "["))
|
||||
double_star_count = sum(1 for part in pattern.split("/") if part == "**")
|
||||
literal_count = sum(char not in "*?[]!" for char in pattern)
|
||||
exact = int(wildcard_count == 0)
|
||||
depth = len(pattern.split("/"))
|
||||
return (exact, literal_count, -double_star_count, -wildcard_count, depth)
|
||||
|
||||
|
||||
def _specificity(entry: dict[str, Any]) -> tuple[int, int, int, int, int, int, int]:
|
||||
scope = entry.get("scope")
|
||||
if not isinstance(scope, dict) or scope.get("all") is True:
|
||||
return (0, 0, 0, 0, 0, 0, 0)
|
||||
populated = 0
|
||||
exact = 0
|
||||
literal = 0
|
||||
double_star = 0
|
||||
wildcard = 0
|
||||
depth = 0
|
||||
extra_or_patterns = 0
|
||||
for field in SCOPE_FIELDS:
|
||||
values = scope.get(field)
|
||||
if isinstance(values, list) and values:
|
||||
populated += 1
|
||||
scores = [
|
||||
_pattern_specificity(value)
|
||||
for value in values
|
||||
if isinstance(value, str)
|
||||
]
|
||||
if scores:
|
||||
dimension_score = min(scores)
|
||||
exact += dimension_score[0]
|
||||
literal += dimension_score[1]
|
||||
double_star += dimension_score[2]
|
||||
wildcard += dimension_score[3]
|
||||
depth += dimension_score[4]
|
||||
extra_or_patterns += len(scores) - 1
|
||||
return (
|
||||
populated,
|
||||
exact,
|
||||
literal,
|
||||
double_star,
|
||||
wildcard,
|
||||
depth,
|
||||
-extra_or_patterns,
|
||||
)
|
||||
|
||||
|
||||
def select_entries(
|
||||
data: dict[str, Any],
|
||||
context: dict[str, list[str]],
|
||||
*,
|
||||
limit: int = DEFAULT_LIMIT,
|
||||
) -> list[dict[str, Any]]:
|
||||
if isinstance(limit, bool) or not isinstance(limit, int) or not 1 <= limit <= MAX_LIMIT:
|
||||
raise ValueError(f"limit 必须在 1..{MAX_LIMIT} 之间")
|
||||
entries = data.get("entries")
|
||||
if not isinstance(entries, list):
|
||||
return []
|
||||
matched = [
|
||||
entry
|
||||
for entry in entries
|
||||
if isinstance(entry, dict)
|
||||
and entry.get("status") == "active"
|
||||
and isinstance(entry.get("scope"), dict)
|
||||
and scope_matches(entry["scope"], context)
|
||||
and stable_ref(entry) is not None
|
||||
]
|
||||
global_entries = [
|
||||
entry
|
||||
for entry in matched
|
||||
if isinstance(entry.get("scope"), dict)
|
||||
and entry["scope"].get("all") is True
|
||||
]
|
||||
scoped_entries = [entry for entry in matched if entry not in global_entries]
|
||||
global_entries.sort(key=lambda entry: stable_ref(entry) or "")
|
||||
if len(global_entries) > limit:
|
||||
raise ValueError(
|
||||
f"命中的全项目知识有 {len(global_entries)} 条,超过 --limit={limit};"
|
||||
"提高 limit 后重试,不能静默丢弃全项目护栏"
|
||||
)
|
||||
scoped_entries.sort(
|
||||
key=lambda entry: (
|
||||
*(-part for part in _specificity(entry)),
|
||||
stable_ref(entry) or "",
|
||||
)
|
||||
)
|
||||
return [
|
||||
*global_entries,
|
||||
*scoped_entries[: limit - len(global_entries)],
|
||||
]
|
||||
|
||||
|
||||
def _parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(description="选择当前任务适用的 active ACK 知识")
|
||||
parser.add_argument(
|
||||
"knowledge", nargs="?", default="knowledge.yaml", help="知识库路径"
|
||||
)
|
||||
parser.add_argument("--component", action="append", default=[])
|
||||
parser.add_argument("--path", action="append", default=[])
|
||||
parser.add_argument("--dependency", action="append", default=[])
|
||||
parser.add_argument("--version", action="append", default=[])
|
||||
parser.add_argument("--tag", action="append", default=[])
|
||||
parser.add_argument("--symbol", action="append", default=[])
|
||||
parser.add_argument("--error-signature", action="append", default=[])
|
||||
parser.add_argument("--limit", type=int, default=DEFAULT_LIMIT)
|
||||
parser.add_argument(
|
||||
"--project-root",
|
||||
help="可选项目根目录,用于 verificationRegistry symlink containment 校验",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--format", choices=("json", "refs"), default="json", dest="output_format"
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
args = _parser().parse_args(argv)
|
||||
knowledge_path = Path(args.knowledge)
|
||||
if not knowledge_path.is_file():
|
||||
sys.stderr.write(f"找不到知识库文件: {knowledge_path}\n")
|
||||
return 2
|
||||
if not 1 <= args.limit <= MAX_LIMIT:
|
||||
sys.stderr.write(f"--limit 必须在 1..{MAX_LIMIT} 之间\n")
|
||||
return 2
|
||||
|
||||
if args.project_root:
|
||||
project_root = Path(args.project_root).expanduser()
|
||||
if not project_root.is_dir():
|
||||
sys.stderr.write(f"项目根目录不存在: {project_root}\n")
|
||||
return 2
|
||||
project_root = project_root.resolve()
|
||||
else:
|
||||
project_root = infer_project_root(knowledge_path)
|
||||
|
||||
data = load_yaml(knowledge_path, "知识库")
|
||||
errors = validate_builtin_structure(data)
|
||||
errors.extend(validate_semantics(data, project_root=project_root))
|
||||
registry = data.get("verificationRegistry")
|
||||
if project_root is None and isinstance(registry, dict) and registry:
|
||||
errors.append(
|
||||
"verificationRegistry 非空但无法确定项目根目录;请传入 --project-root"
|
||||
)
|
||||
errors = list(dict.fromkeys(errors))
|
||||
if errors:
|
||||
sys.stderr.write(f"知识库无效,拒绝选择,共 {len(errors)} 项:\n")
|
||||
for error in errors:
|
||||
sys.stderr.write(f" - {error}\n")
|
||||
return 1
|
||||
|
||||
context = {
|
||||
"components": args.component,
|
||||
"paths": args.path,
|
||||
"dependencies": args.dependency,
|
||||
"versions": args.version,
|
||||
"tags": args.tag,
|
||||
"symbols": args.symbol,
|
||||
"errorSignatures": args.error_signature,
|
||||
}
|
||||
try:
|
||||
selected = select_entries(data, context, limit=args.limit)
|
||||
except ValueError as exc:
|
||||
sys.stderr.write(f"知识选择失败: {exc}\n")
|
||||
return 1
|
||||
refs = [stable_ref(entry) for entry in selected]
|
||||
if args.output_format == "refs":
|
||||
if refs:
|
||||
sys.stdout.write("\n".join(ref for ref in refs if ref) + "\n")
|
||||
return 0
|
||||
|
||||
payload = {
|
||||
"count": len(selected),
|
||||
"limit": args.limit,
|
||||
"refs": refs,
|
||||
"entries": [
|
||||
{
|
||||
"ref": stable_ref(entry),
|
||||
**entry,
|
||||
"verificationTarget": (
|
||||
data.get("verificationRegistry", {}).get(
|
||||
entry.get("verification", {}).get("ref")
|
||||
)
|
||||
if isinstance(data.get("verificationRegistry"), dict)
|
||||
and isinstance(entry.get("verification"), dict)
|
||||
else None
|
||||
),
|
||||
}
|
||||
for entry in selected
|
||||
],
|
||||
}
|
||||
sys.stdout.write(json.dumps(payload, ensure_ascii=False, indent=2) + "\n")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Executable
+1152
File diff suppressed because it is too large
Load Diff
@@ -3,9 +3,11 @@
|
||||
|
||||
权威结构是同目录上层的 templates/tasks.schema.json(跨语言可用)。
|
||||
本脚本是参考实现:
|
||||
- 若安装了 jsonschema,则用 schema 做完整校验;
|
||||
- 否则回退到内置的关键规则校验(必填字段、状态枚举、三轮上限、leftover 留档)。
|
||||
YAML 解析优先用 pyyaml;未安装时给出提示而非崩溃。
|
||||
- 始终执行内置语义校验;
|
||||
- 安装了 jsonschema 时,再叠加 schema 结构校验;
|
||||
- knowledge 字段会检查引用格式、候选结构和 Test 检查结果。
|
||||
YAML 优先使用 PyYAML;未安装时使用 fail-closed 的 ACK YAML 子集。
|
||||
JSON 任务板只使用标准库,两种格式都拒绝重复键。
|
||||
|
||||
用法:
|
||||
python3 validate_tasks.py [tasks.yaml]
|
||||
@@ -18,9 +20,18 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from yaml_subset import (
|
||||
DuplicateKeyError,
|
||||
YamlSubsetError,
|
||||
load_json_unique,
|
||||
load_yaml_subset,
|
||||
make_unique_pyyaml_loader,
|
||||
)
|
||||
|
||||
STATUS_ENUM = {
|
||||
"open",
|
||||
"dispatched",
|
||||
@@ -32,29 +43,337 @@ STATUS_ENUM = {
|
||||
"leftover",
|
||||
}
|
||||
MAX_ROUNDS = 3
|
||||
KNOWLEDGE_KINDS = {"guardrail", "pitfall", "verification"}
|
||||
KNOWLEDGE_CHECK_RESULTS = {"passed", "failed", "not_applicable"}
|
||||
KNOWLEDGE_REF_RE = re.compile(r"^K-[A-Z0-9][A-Z0-9-]*@[1-9][0-9]*$")
|
||||
ATTEMPT_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*-A[1-9][0-9]*$")
|
||||
KNOWLEDGE_SCOPE_FIELDS = {
|
||||
"components",
|
||||
"paths",
|
||||
"dependencies",
|
||||
"versions",
|
||||
"tags",
|
||||
"symbols",
|
||||
"errorSignatures",
|
||||
}
|
||||
KNOWLEDGE_APPLICATION_FIELDS = {"ref", "result", "evidence"}
|
||||
KNOWLEDGE_CANDIDATE_FIELDS = {
|
||||
"kind",
|
||||
"title",
|
||||
"claim",
|
||||
"scope",
|
||||
"appliesWhen",
|
||||
"directive",
|
||||
"rationale",
|
||||
"evidenceRefs",
|
||||
"proposedBy",
|
||||
"proposedAt",
|
||||
}
|
||||
KNOWLEDGE_CHECK_FIELDS = {
|
||||
"ref",
|
||||
"result",
|
||||
"evidence",
|
||||
"checkedBy",
|
||||
"checkedAt",
|
||||
}
|
||||
KNOWLEDGE_CANDIDATE_REQUIRED_FIELDS = {
|
||||
"kind",
|
||||
"title",
|
||||
"claim",
|
||||
"scope",
|
||||
"appliesWhen",
|
||||
"directive",
|
||||
"rationale",
|
||||
"evidenceRefs",
|
||||
}
|
||||
KNOWLEDGE_CANDIDATE_TEXT_FIELDS = {
|
||||
"title",
|
||||
"claim",
|
||||
"appliesWhen",
|
||||
"directive",
|
||||
"rationale",
|
||||
}
|
||||
|
||||
|
||||
def load_yaml(path: Path) -> dict:
|
||||
def _nonempty_string(value: object) -> bool:
|
||||
return isinstance(value, str) and bool(value.strip())
|
||||
|
||||
|
||||
def load_document(path: Path) -> dict:
|
||||
try:
|
||||
import yaml # type: ignore
|
||||
except ImportError:
|
||||
sys.stderr.write(
|
||||
"需要 PyYAML 才能解析 YAML:pip install pyyaml\n"
|
||||
"(或把任务板导出为 JSON 后再校验)\n"
|
||||
)
|
||||
raise SystemExit(2)
|
||||
try:
|
||||
with path.open(encoding="utf-8") as fh:
|
||||
data = yaml.safe_load(fh)
|
||||
except yaml.YAMLError as exc: # type: ignore
|
||||
sys.stderr.write(f"YAML 解析失败: {exc}\n")
|
||||
content = path.read_text(encoding="utf-8")
|
||||
except OSError as exc:
|
||||
sys.stderr.write(f"任务板读取失败: {exc}\n")
|
||||
raise SystemExit(1)
|
||||
|
||||
if path.suffix.lower() == ".json":
|
||||
try:
|
||||
data = load_json_unique(content)
|
||||
except (json.JSONDecodeError, DuplicateKeyError) as exc:
|
||||
sys.stderr.write(f"JSON 解析失败: {exc}\n")
|
||||
raise SystemExit(1)
|
||||
else:
|
||||
try:
|
||||
import yaml # type: ignore
|
||||
except ImportError:
|
||||
try:
|
||||
data = load_yaml_subset(content)
|
||||
except YamlSubsetError as exc:
|
||||
sys.stderr.write(f"YAML 子集解析失败: {exc}\n")
|
||||
raise SystemExit(1)
|
||||
else:
|
||||
try:
|
||||
data = yaml.load(
|
||||
content,
|
||||
Loader=make_unique_pyyaml_loader(yaml),
|
||||
)
|
||||
except yaml.YAMLError as exc: # type: ignore
|
||||
sys.stderr.write(f"YAML 解析失败: {exc}\n")
|
||||
raise SystemExit(1)
|
||||
if not isinstance(data, dict):
|
||||
sys.stderr.write("任务板顶层必须是对象(mapping)\n")
|
||||
raise SystemExit(1)
|
||||
return data
|
||||
|
||||
|
||||
def validate_knowledge_ref_list(
|
||||
value: object,
|
||||
where: str,
|
||||
errors: list[str],
|
||||
) -> list[str]:
|
||||
if not isinstance(value, list):
|
||||
errors.append(f"{where}: 必须是列表")
|
||||
return []
|
||||
|
||||
refs: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for index, ref in enumerate(value):
|
||||
item_where = f"{where}[{index}]"
|
||||
if not isinstance(ref, str) or not KNOWLEDGE_REF_RE.fullmatch(ref):
|
||||
errors.append(f"{item_where}: 必须使用 K-<id>@<revision> 格式")
|
||||
continue
|
||||
if ref in seen:
|
||||
errors.append(f"{item_where}: 引用重复: {ref}")
|
||||
seen.add(ref)
|
||||
refs.append(ref)
|
||||
return refs
|
||||
|
||||
|
||||
def reject_unknown_fields(
|
||||
value: dict,
|
||||
allowed: set[str],
|
||||
where: str,
|
||||
errors: list[str],
|
||||
) -> None:
|
||||
"""Mirror additionalProperties=false for builtin knowledge validation."""
|
||||
for field in sorted(set(value) - allowed):
|
||||
errors.append(f"{where}: 未知字段 {field!r}")
|
||||
|
||||
|
||||
def validate_optional_string_fields(
|
||||
value: dict,
|
||||
fields: set[str],
|
||||
where: str,
|
||||
errors: list[str],
|
||||
) -> None:
|
||||
for field in sorted(fields):
|
||||
if field in value and not isinstance(value[field], str):
|
||||
errors.append(f"{where}.{field}: 必须是字符串")
|
||||
|
||||
|
||||
def validate_knowledge_fields(
|
||||
task: dict,
|
||||
where: str,
|
||||
status: object,
|
||||
errors: list[str],
|
||||
) -> None:
|
||||
refs = (
|
||||
validate_knowledge_ref_list(
|
||||
task["knowledgeRefs"],
|
||||
f"{where}.knowledgeRefs",
|
||||
errors,
|
||||
)
|
||||
if "knowledgeRefs" in task
|
||||
else []
|
||||
)
|
||||
if "knowledgeApplied" in task:
|
||||
applications = task["knowledgeApplied"]
|
||||
if not isinstance(applications, list):
|
||||
errors.append(f"{where}.knowledgeApplied: 必须是列表")
|
||||
else:
|
||||
seen_applications: set[str] = set()
|
||||
for index, application in enumerate(applications):
|
||||
application_where = f"{where}.knowledgeApplied[{index}]"
|
||||
if not isinstance(application, dict):
|
||||
errors.append(f"{application_where}: 必须是对象")
|
||||
continue
|
||||
reject_unknown_fields(
|
||||
application,
|
||||
KNOWLEDGE_APPLICATION_FIELDS,
|
||||
application_where,
|
||||
errors,
|
||||
)
|
||||
ref = application.get("ref")
|
||||
if not isinstance(ref, str) or not KNOWLEDGE_REF_RE.fullmatch(ref):
|
||||
errors.append(
|
||||
f"{application_where}.ref: 必须使用 K-<id>@<revision> 格式"
|
||||
)
|
||||
else:
|
||||
if ref in seen_applications:
|
||||
errors.append(f"{application_where}.ref: 应用结果重复: {ref}")
|
||||
seen_applications.add(ref)
|
||||
if ref not in refs:
|
||||
errors.append(f"{application_where}.ref: {ref} 不在 knowledgeRefs 中")
|
||||
if application.get("result") not in {"applied", "not_applicable"}:
|
||||
errors.append(
|
||||
f"{application_where}.result: 必须是 applied/not_applicable"
|
||||
)
|
||||
evidence = application.get("evidence")
|
||||
if not isinstance(evidence, str) or not evidence.strip():
|
||||
errors.append(f"{application_where}.evidence: 必须提供非空证据")
|
||||
|
||||
if "knowledgeCandidates" in task:
|
||||
candidates = task["knowledgeCandidates"]
|
||||
if not isinstance(candidates, list):
|
||||
errors.append(f"{where}.knowledgeCandidates: 必须是列表")
|
||||
else:
|
||||
for index, candidate in enumerate(candidates):
|
||||
candidate_where = f"{where}.knowledgeCandidates[{index}]"
|
||||
if not isinstance(candidate, dict):
|
||||
errors.append(f"{candidate_where}: 必须是对象")
|
||||
continue
|
||||
reject_unknown_fields(
|
||||
candidate,
|
||||
KNOWLEDGE_CANDIDATE_FIELDS,
|
||||
candidate_where,
|
||||
errors,
|
||||
)
|
||||
missing = sorted(
|
||||
key
|
||||
for key in KNOWLEDGE_CANDIDATE_REQUIRED_FIELDS
|
||||
if key not in candidate
|
||||
)
|
||||
if missing:
|
||||
errors.append(
|
||||
f"{candidate_where}: 缺少必填字段 {', '.join(missing)}"
|
||||
)
|
||||
if candidate.get("kind") not in KNOWLEDGE_KINDS:
|
||||
errors.append(
|
||||
f"{candidate_where}.kind: 必须是 {sorted(KNOWLEDGE_KINDS)}"
|
||||
)
|
||||
for field in sorted(KNOWLEDGE_CANDIDATE_TEXT_FIELDS):
|
||||
value = candidate.get(field)
|
||||
if not isinstance(value, str) or not value.strip():
|
||||
errors.append(
|
||||
f"{candidate_where}.{field}: 必须是非空字符串"
|
||||
)
|
||||
validate_optional_string_fields(
|
||||
candidate,
|
||||
{"proposedBy", "proposedAt"},
|
||||
candidate_where,
|
||||
errors,
|
||||
)
|
||||
scope = candidate.get("scope")
|
||||
if not isinstance(scope, dict):
|
||||
errors.append(f"{candidate_where}.scope: 至少包含一个非空作用域")
|
||||
else:
|
||||
unknown_scope_fields = sorted(
|
||||
set(scope) - KNOWLEDGE_SCOPE_FIELDS - {"all"}
|
||||
)
|
||||
for field in unknown_scope_fields:
|
||||
errors.append(
|
||||
f"{candidate_where}.scope: 未知字段 {field!r}"
|
||||
)
|
||||
if "all" in scope and not isinstance(scope.get("all"), bool):
|
||||
errors.append(f"{candidate_where}.scope.all: 必须是布尔值")
|
||||
populated_scope_fields: list[str] = []
|
||||
for field in sorted(KNOWLEDGE_SCOPE_FIELDS & set(scope)):
|
||||
values = scope.get(field)
|
||||
if not isinstance(values, list) or any(
|
||||
not isinstance(item, str) or not item.strip()
|
||||
for item in values
|
||||
):
|
||||
errors.append(
|
||||
f"{candidate_where}.scope.{field}: "
|
||||
"必须是非空字符串列表"
|
||||
)
|
||||
elif len(values) != len(set(values)):
|
||||
errors.append(
|
||||
f"{candidate_where}.scope.{field}: 不能包含重复值"
|
||||
)
|
||||
elif values:
|
||||
populated_scope_fields.append(field)
|
||||
if scope.get("all") is True and populated_scope_fields:
|
||||
errors.append(
|
||||
f"{candidate_where}.scope: all=true 时不能同时填写作用域维度"
|
||||
)
|
||||
if scope.get("all") is not True and not populated_scope_fields:
|
||||
errors.append(
|
||||
f"{candidate_where}.scope: 至少包含一个非空作用域"
|
||||
)
|
||||
evidence_refs = candidate.get("evidenceRefs")
|
||||
if (
|
||||
not isinstance(evidence_refs, list)
|
||||
or not evidence_refs
|
||||
or not all(
|
||||
isinstance(ref, str) and ref.strip()
|
||||
for ref in evidence_refs
|
||||
)
|
||||
):
|
||||
errors.append(
|
||||
f"{candidate_where}.evidenceRefs: 必须是非空字符串列表"
|
||||
)
|
||||
elif len(evidence_refs) != len(set(evidence_refs)):
|
||||
errors.append(
|
||||
f"{candidate_where}.evidenceRefs: 不能包含重复值"
|
||||
)
|
||||
|
||||
if "knowledgeChecks" not in task:
|
||||
return
|
||||
checks = task["knowledgeChecks"]
|
||||
if not isinstance(checks, list):
|
||||
errors.append(f"{where}.knowledgeChecks: 必须是列表")
|
||||
return
|
||||
|
||||
seen_checks: set[str] = set()
|
||||
for index, check in enumerate(checks):
|
||||
check_where = f"{where}.knowledgeChecks[{index}]"
|
||||
if not isinstance(check, dict):
|
||||
errors.append(f"{check_where}: 必须是对象")
|
||||
continue
|
||||
reject_unknown_fields(
|
||||
check,
|
||||
KNOWLEDGE_CHECK_FIELDS,
|
||||
check_where,
|
||||
errors,
|
||||
)
|
||||
validate_optional_string_fields(
|
||||
check,
|
||||
{"checkedBy", "checkedAt"},
|
||||
check_where,
|
||||
errors,
|
||||
)
|
||||
ref = check.get("ref")
|
||||
result = check.get("result")
|
||||
evidence = check.get("evidence")
|
||||
if not isinstance(ref, str) or not KNOWLEDGE_REF_RE.fullmatch(ref):
|
||||
errors.append(f"{check_where}.ref: 必须使用 K-<id>@<revision> 格式")
|
||||
else:
|
||||
if ref in seen_checks:
|
||||
errors.append(f"{check_where}.ref: 检查结果重复: {ref}")
|
||||
seen_checks.add(ref)
|
||||
if ref not in refs:
|
||||
errors.append(f"{check_where}.ref: {ref} 不在 knowledgeRefs 中")
|
||||
if result not in KNOWLEDGE_CHECK_RESULTS:
|
||||
errors.append(
|
||||
f"{check_where}.result: 必须是 {sorted(KNOWLEDGE_CHECK_RESULTS)}"
|
||||
)
|
||||
if not isinstance(evidence, str) or not evidence.strip():
|
||||
errors.append(f"{check_where}.evidence: 必须提供非空证据")
|
||||
if status == "verified" and result == "failed":
|
||||
errors.append(f"{check_where}: verified 任务不能保留失败的知识检查")
|
||||
|
||||
|
||||
def validate_with_schema(data: dict, schema_path: Path) -> list[str]:
|
||||
import jsonschema # type: ignore
|
||||
|
||||
@@ -70,11 +389,92 @@ def validate_with_schema(data: dict, schema_path: Path) -> list[str]:
|
||||
def validate_builtin(data: dict) -> list[str]:
|
||||
errors: list[str] = []
|
||||
|
||||
if not isinstance(data.get("version"), int) or data.get("version", 0) < 1:
|
||||
def validate_string_fields(
|
||||
value: dict,
|
||||
fields: set[str],
|
||||
where: str,
|
||||
*,
|
||||
nullable: bool = False,
|
||||
) -> None:
|
||||
expected = "必须是字符串或 null" if nullable else "必须是字符串"
|
||||
for field in sorted(fields):
|
||||
if field not in value:
|
||||
continue
|
||||
field_value = value[field]
|
||||
if not isinstance(field_value, str) and not (
|
||||
nullable and field_value is None
|
||||
):
|
||||
errors.append(f"{where}.{field}: {expected}")
|
||||
|
||||
def validate_string_lists(
|
||||
value: dict,
|
||||
fields: set[str],
|
||||
where: str,
|
||||
) -> None:
|
||||
for field in sorted(fields):
|
||||
if field not in value:
|
||||
continue
|
||||
items = value[field]
|
||||
if not isinstance(items, list):
|
||||
errors.append(f"{where}.{field}: 必须是列表")
|
||||
elif any(not isinstance(item, str) for item in items):
|
||||
errors.append(f"{where}.{field}: 列表项必须是字符串")
|
||||
|
||||
def validate_object_fields(
|
||||
value: dict,
|
||||
fields: set[str],
|
||||
where: str,
|
||||
) -> None:
|
||||
for field in sorted(fields):
|
||||
if field in value and not isinstance(value[field], dict):
|
||||
errors.append(f"{where}.{field}: 必须是对象")
|
||||
|
||||
version = data.get("version")
|
||||
if (
|
||||
not isinstance(version, int)
|
||||
or isinstance(version, bool)
|
||||
or version < 1
|
||||
):
|
||||
errors.append("version 必须是 >=1 的整数")
|
||||
validate_string_fields(
|
||||
data,
|
||||
{"updatedAt", "source", "ackVersion", "kitVersion"},
|
||||
"<root>",
|
||||
)
|
||||
|
||||
project = data.get("project")
|
||||
if not isinstance(project, dict) or not project.get("name"):
|
||||
errors.append("project.name 必填")
|
||||
if not isinstance(project, dict):
|
||||
errors.append("project 必须是对象")
|
||||
else:
|
||||
if not _nonempty_string(project.get("name")):
|
||||
errors.append("project.name 必须是非空字符串")
|
||||
validate_string_fields(
|
||||
project,
|
||||
{"repoPath", "baseUrl", "devWorktree", "overlayFile"},
|
||||
"project",
|
||||
)
|
||||
if (
|
||||
"knowledgeFile" in project
|
||||
and project.get("knowledgeFile") != "docs/ack/knowledge.yaml"
|
||||
):
|
||||
errors.append(
|
||||
"project.knowledgeFile 必须固定为 docs/ack/knowledge.yaml"
|
||||
)
|
||||
|
||||
if "summary" in data:
|
||||
summary = data["summary"]
|
||||
if not isinstance(summary, dict):
|
||||
errors.append("summary 必须是对象")
|
||||
else:
|
||||
validate_string_lists(
|
||||
summary,
|
||||
{"verified", "open", "failedRetest", "leftovers"},
|
||||
"summary",
|
||||
)
|
||||
if "statusReference" in data and not isinstance(
|
||||
data["statusReference"], dict
|
||||
):
|
||||
errors.append("statusReference 必须是对象")
|
||||
|
||||
tasks = data.get("tasks")
|
||||
if not isinstance(tasks, list):
|
||||
@@ -90,34 +490,148 @@ def validate_builtin(data: dict) -> list[str]:
|
||||
tid = task.get("id")
|
||||
title = task.get("title")
|
||||
status = task.get("status")
|
||||
if not tid:
|
||||
errors.append(f"{where}: id 必填")
|
||||
if not _nonempty_string(tid):
|
||||
errors.append(f"{where}: id 必须是非空字符串")
|
||||
else:
|
||||
where = f"tasks[{i}] {tid}"
|
||||
if tid in seen_ids:
|
||||
errors.append(f"{where}: id 重复")
|
||||
seen_ids.add(tid)
|
||||
if not title:
|
||||
errors.append(f"{where}: title 必填")
|
||||
if not _nonempty_string(title):
|
||||
errors.append(f"{where}: title 必须是非空字符串")
|
||||
if status not in STATUS_ENUM:
|
||||
errors.append(
|
||||
f"{where}: status={status!r} 非法,应为 {sorted(STATUS_ENUM)}"
|
||||
)
|
||||
|
||||
dispatch = task.get("dispatch") or {}
|
||||
rounds = dispatch.get("rounds") or []
|
||||
if isinstance(rounds, list):
|
||||
validate_string_fields(
|
||||
task,
|
||||
{
|
||||
"type",
|
||||
"priority",
|
||||
"assignee",
|
||||
"component",
|
||||
"description",
|
||||
"expected",
|
||||
"actual",
|
||||
},
|
||||
where,
|
||||
)
|
||||
validate_string_lists(
|
||||
task,
|
||||
{"specRefs", "testRefs", "stepsToReproduce"},
|
||||
where,
|
||||
)
|
||||
validate_object_fields(task, {"evidence", "verification"}, where)
|
||||
|
||||
validate_knowledge_fields(task, where, status, errors)
|
||||
|
||||
if "dispatch" not in task:
|
||||
dispatch = {}
|
||||
elif not isinstance(task["dispatch"], dict):
|
||||
errors.append(f"{where}.dispatch: 必须是对象")
|
||||
dispatch = {}
|
||||
else:
|
||||
dispatch = task["dispatch"]
|
||||
validate_string_fields(
|
||||
dispatch,
|
||||
{"taskId", "dispatchId", "worker"},
|
||||
f"{where}.dispatch",
|
||||
nullable=True,
|
||||
)
|
||||
|
||||
rounds = dispatch.get("rounds", [])
|
||||
if not isinstance(rounds, list):
|
||||
errors.append(f"{where}.dispatch.rounds: 必须是列表")
|
||||
else:
|
||||
if len(rounds) > MAX_ROUNDS:
|
||||
errors.append(
|
||||
f"{where}: 派发轮次 {len(rounds)} 超过上限 {MAX_ROUNDS}"
|
||||
)
|
||||
for r in rounds:
|
||||
if isinstance(r, dict) and r.get("result") not in {"passed", "failed"}:
|
||||
errors.append(f"{where}: round.result 必须是 passed/failed")
|
||||
seen_attempt_ids: set[str] = set()
|
||||
round_numbers: list[int] = []
|
||||
for round_index, round_item in enumerate(rounds):
|
||||
round_where = f"{where}.dispatch.rounds[{round_index}]"
|
||||
if not isinstance(round_item, dict):
|
||||
errors.append(f"{round_where}: 必须是对象")
|
||||
continue
|
||||
if round_item.get("result") not in {"passed", "failed"}:
|
||||
errors.append(f"{round_where}.result: 必须是 passed/failed")
|
||||
if "evidence" in round_item and not isinstance(
|
||||
round_item["evidence"], str
|
||||
):
|
||||
errors.append(f"{round_where}.evidence: 必须是字符串")
|
||||
round_number = round_item.get("round")
|
||||
round_number_is_valid = (
|
||||
isinstance(round_number, int)
|
||||
and not isinstance(round_number, bool)
|
||||
and 1 <= round_number <= MAX_ROUNDS
|
||||
)
|
||||
if not round_number_is_valid:
|
||||
errors.append(
|
||||
f"{round_where}.round: 必须是 1..{MAX_ROUNDS} 的整数"
|
||||
)
|
||||
else:
|
||||
round_numbers.append(round_number)
|
||||
if "attemptId" in round_item:
|
||||
attempt_id = round_item["attemptId"]
|
||||
if (
|
||||
not isinstance(attempt_id, str)
|
||||
or not ATTEMPT_ID_RE.fullmatch(attempt_id)
|
||||
):
|
||||
errors.append(
|
||||
f"{round_where}.attemptId: "
|
||||
"必须使用 <task-id>-A<round> 格式"
|
||||
)
|
||||
else:
|
||||
if attempt_id in seen_attempt_ids:
|
||||
errors.append(
|
||||
f"{round_where}.attemptId: "
|
||||
f"轮次内不能重复: {attempt_id}"
|
||||
)
|
||||
seen_attempt_ids.add(attempt_id)
|
||||
if (
|
||||
isinstance(tid, str)
|
||||
and round_number_is_valid
|
||||
and attempt_id != f"{tid}-A{round_number}"
|
||||
):
|
||||
errors.append(
|
||||
f"{round_where}.attemptId: 应为 "
|
||||
f"{tid}-A{round_number}"
|
||||
)
|
||||
expected_rounds = list(range(1, len(rounds) + 1))
|
||||
if round_numbers != expected_rounds:
|
||||
errors.append(
|
||||
f"{where}.dispatch.rounds: round 必须从 1 连续递增且不重复"
|
||||
)
|
||||
|
||||
resolution = task.get("resolution")
|
||||
if "resolution" in task:
|
||||
if not isinstance(resolution, dict):
|
||||
errors.append(f"{where}.resolution: 必须是对象")
|
||||
else:
|
||||
validate_string_fields(
|
||||
resolution,
|
||||
{
|
||||
"fixedBy",
|
||||
"verifiedBy",
|
||||
"verifiedAt",
|
||||
"leftoverReason",
|
||||
},
|
||||
f"{where}.resolution",
|
||||
nullable=True,
|
||||
)
|
||||
validate_object_fields(
|
||||
resolution,
|
||||
{"evidence"},
|
||||
f"{where}.resolution",
|
||||
)
|
||||
|
||||
if status == "leftover":
|
||||
resolution = task.get("resolution") or {}
|
||||
if not resolution.get("leftoverReason"):
|
||||
if (
|
||||
not isinstance(resolution, dict)
|
||||
or not _nonempty_string(resolution.get("leftoverReason"))
|
||||
):
|
||||
errors.append(f"{where}: leftover 必须填 resolution.leftoverReason")
|
||||
|
||||
return errors
|
||||
@@ -134,24 +648,27 @@ def main(argv: list[str] | None = None) -> int:
|
||||
sys.stderr.write(f"找不到任务板文件: {tasks_path}\n")
|
||||
return 2
|
||||
|
||||
data = load_yaml(tasks_path)
|
||||
data = load_document(tasks_path)
|
||||
|
||||
schema_path = Path(args.schema) if args.schema else (
|
||||
Path(__file__).resolve().parent.parent / "templates" / "tasks.schema.json"
|
||||
)
|
||||
if args.schema and not schema_path.is_file():
|
||||
sys.stderr.write(f"找不到指定的 schema 文件: {schema_path}\n")
|
||||
return 2
|
||||
|
||||
mode = "内置规则"
|
||||
errors = validate_builtin(data)
|
||||
mode = "内置语义规则"
|
||||
try:
|
||||
import jsonschema # type: ignore # noqa: F401
|
||||
|
||||
if schema_path.is_file():
|
||||
errors = validate_with_schema(data, schema_path)
|
||||
mode = f"schema ({schema_path.name})"
|
||||
errors = validate_with_schema(data, schema_path) + errors
|
||||
mode = f"schema ({schema_path.name}) + 内置语义规则"
|
||||
else:
|
||||
errors = validate_builtin(data)
|
||||
mode = "内置规则(未找到 schema 文件)"
|
||||
mode = "内置语义规则(未找到 schema 文件)"
|
||||
except ImportError:
|
||||
errors = validate_builtin(data)
|
||||
pass
|
||||
|
||||
if errors:
|
||||
sys.stderr.write(f"任务板校验失败({mode}),共 {len(errors)} 项:\n")
|
||||
|
||||
@@ -0,0 +1,686 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ACK YAML 的零依赖、fail-closed 子集解析器。
|
||||
|
||||
这不是通用 YAML 实现。它只覆盖 ACK 状态文件所需的 mapping、
|
||||
sequence、flow collection、标量和 ``>`` / ``|`` block scalar。锚点、
|
||||
alias、tag、多文档和其它未实现语法会显式失败,不会猜测或静默误解析。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
|
||||
class YamlSubsetError(ValueError):
|
||||
"""YAML 超出 ACK 子集或语法无效。"""
|
||||
|
||||
|
||||
class DuplicateKeyError(ValueError):
|
||||
"""JSON/YAML mapping 包含重复键。"""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class _Line:
|
||||
number: int
|
||||
indent: int
|
||||
content: str
|
||||
|
||||
|
||||
_DECIMAL_INT_RE = re.compile(r"[-+]?(?:0|[1-9][0-9]*)\Z")
|
||||
_AMBIGUOUS_NUMBER_RE = re.compile(
|
||||
r"[-+]?(?:"
|
||||
r"[0-9][0-9_]*\.[0-9_]*(?:[eE][-+]?[0-9]+)?|"
|
||||
r"[0-9][0-9_]*(?:[eE][-+]?[0-9]+)|"
|
||||
r"0[xX][0-9a-fA-F_]+|0[oO][0-7_]+|0[bB][01_]+|"
|
||||
r"0[0-9_]+|[0-9][0-9_]*:[0-9_:]+"
|
||||
r")\Z"
|
||||
)
|
||||
_PLAIN_KEY_FORBIDDEN_RE = re.compile(r"[\[\]{},#]")
|
||||
_ANCHOR_OR_ALIAS_RE = re.compile(
|
||||
r"(?:^|\s)[&*][A-Za-z0-9_-]+(?:\s|$)"
|
||||
)
|
||||
|
||||
|
||||
def load_json_unique(content: str) -> Any:
|
||||
"""Parse JSON while rejecting duplicate object keys at every depth."""
|
||||
|
||||
def unique_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
|
||||
result: dict[str, Any] = {}
|
||||
for key, value in pairs:
|
||||
if key in result:
|
||||
raise DuplicateKeyError(f"JSON 存在重复键 {key!r}")
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
return json.loads(content, object_pairs_hook=unique_object)
|
||||
|
||||
|
||||
def make_unique_pyyaml_loader(yaml_module: Any) -> type:
|
||||
"""Build a SafeLoader that rejects duplicate keys and graph features.
|
||||
|
||||
ACK documents are trees. Anchors/aliases can introduce shared identity or
|
||||
cycles, which are unnecessary here and make recursive validation unsafe.
|
||||
Explicit tags are also outside the fallback grammar, so both code paths
|
||||
reject them consistently.
|
||||
"""
|
||||
|
||||
class UniqueKeySafeLoader(yaml_module.SafeLoader): # type: ignore[misc]
|
||||
def compose_node(self, parent: Any, index: Any) -> Any:
|
||||
if self.check_event(yaml_module.events.AliasEvent):
|
||||
event = self.peek_event()
|
||||
raise yaml_module.YAMLError(
|
||||
f"ACK YAML 不支持 alias: *{event.anchor}"
|
||||
)
|
||||
event = self.peek_event()
|
||||
if getattr(event, "anchor", None) is not None:
|
||||
raise yaml_module.YAMLError(
|
||||
f"ACK YAML 不支持 anchor: &{event.anchor}"
|
||||
)
|
||||
if getattr(event, "tag", None) is not None:
|
||||
raise yaml_module.YAMLError(
|
||||
f"ACK YAML 不支持显式 tag: {event.tag}"
|
||||
)
|
||||
return super().compose_node(parent, index)
|
||||
|
||||
def construct_unique_mapping(
|
||||
loader: Any,
|
||||
node: Any,
|
||||
deep: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
for key_node, _ in node.value:
|
||||
if (
|
||||
getattr(key_node, "tag", None) == "tag:yaml.org,2002:merge"
|
||||
or getattr(key_node, "value", None) == "<<"
|
||||
):
|
||||
raise yaml_module.constructor.ConstructorError(
|
||||
"while constructing an ACK mapping",
|
||||
node.start_mark,
|
||||
"merge keys are not supported",
|
||||
key_node.start_mark,
|
||||
)
|
||||
loader.flatten_mapping(node)
|
||||
mapping: dict[str, Any] = {}
|
||||
for key_node, value_node in node.value:
|
||||
key = loader.construct_object(key_node, deep=deep)
|
||||
if not isinstance(key, str):
|
||||
raise yaml_module.constructor.ConstructorError(
|
||||
"while constructing an ACK mapping",
|
||||
node.start_mark,
|
||||
"mapping key must be a string",
|
||||
key_node.start_mark,
|
||||
)
|
||||
if key == "<<":
|
||||
raise yaml_module.constructor.ConstructorError(
|
||||
"while constructing an ACK mapping",
|
||||
node.start_mark,
|
||||
"merge keys are not supported",
|
||||
key_node.start_mark,
|
||||
)
|
||||
if key in mapping:
|
||||
raise yaml_module.constructor.ConstructorError(
|
||||
"while constructing an ACK mapping",
|
||||
node.start_mark,
|
||||
f"found duplicate key {key!r}",
|
||||
key_node.start_mark,
|
||||
)
|
||||
mapping[key] = loader.construct_object(value_node, deep=deep)
|
||||
return mapping
|
||||
|
||||
UniqueKeySafeLoader.add_constructor(
|
||||
yaml_module.resolver.BaseResolver.DEFAULT_MAPPING_TAG,
|
||||
construct_unique_mapping,
|
||||
)
|
||||
return UniqueKeySafeLoader
|
||||
|
||||
|
||||
def load_yaml_subset(content: str) -> Any:
|
||||
"""Parse the deliberately small YAML subset used by ACK files."""
|
||||
|
||||
return _SubsetParser(content).parse()
|
||||
|
||||
|
||||
class _SubsetParser:
|
||||
def __init__(self, content: str) -> None:
|
||||
if content.startswith("\ufeff"):
|
||||
content = content[1:]
|
||||
if "\t" in content:
|
||||
raise YamlSubsetError("ACK YAML 子集不支持 Tab,请使用空格")
|
||||
self.lines = [
|
||||
_Line(number, len(raw) - len(raw.lstrip(" ")), raw.lstrip(" "))
|
||||
for number, raw in enumerate(content.splitlines(), start=1)
|
||||
]
|
||||
self.index = 0
|
||||
|
||||
def parse(self) -> Any:
|
||||
self._skip_insignificant()
|
||||
if self.index >= len(self.lines):
|
||||
return None
|
||||
first = self.lines[self.index]
|
||||
if first.indent != 0:
|
||||
self._error(first, "顶层不能缩进")
|
||||
value = self._parse_node(0)
|
||||
self._skip_insignificant()
|
||||
if self.index != len(self.lines):
|
||||
line = self.lines[self.index]
|
||||
self._error(line, "文档尾部存在无法解析的内容")
|
||||
return value
|
||||
|
||||
def _parse_node(self, indent: int) -> Any:
|
||||
self._skip_insignificant()
|
||||
if self.index >= len(self.lines):
|
||||
return None
|
||||
line = self.lines[self.index]
|
||||
if line.indent != indent:
|
||||
self._error(line, f"期望 {indent} 个空格的缩进")
|
||||
content = self._without_comment(line.content).rstrip()
|
||||
self._reject_document_syntax(content, line)
|
||||
if self._is_sequence_marker(content):
|
||||
return self._parse_sequence(indent)
|
||||
return self._parse_mapping(indent)
|
||||
|
||||
def _parse_mapping(self, indent: int) -> dict[str, Any]:
|
||||
result: dict[str, Any] = {}
|
||||
while True:
|
||||
self._skip_insignificant()
|
||||
if self.index >= len(self.lines):
|
||||
break
|
||||
line = self.lines[self.index]
|
||||
if line.indent < indent:
|
||||
break
|
||||
if line.indent > indent:
|
||||
self._error(line, "mapping 存在意外缩进")
|
||||
content = self._without_comment(line.content).rstrip()
|
||||
self._reject_document_syntax(content, line)
|
||||
if self._is_sequence_marker(content):
|
||||
break
|
||||
self.index += 1
|
||||
self._consume_mapping_entry(
|
||||
result,
|
||||
content,
|
||||
mapping_indent=indent,
|
||||
line=line,
|
||||
)
|
||||
return result
|
||||
|
||||
def _parse_sequence(self, indent: int) -> list[Any]:
|
||||
result: list[Any] = []
|
||||
while True:
|
||||
self._skip_insignificant()
|
||||
if self.index >= len(self.lines):
|
||||
break
|
||||
line = self.lines[self.index]
|
||||
if line.indent < indent:
|
||||
break
|
||||
if line.indent > indent:
|
||||
self._error(line, "sequence 存在意外缩进")
|
||||
content = self._without_comment(line.content).rstrip()
|
||||
self._reject_document_syntax(content, line)
|
||||
if not self._is_sequence_marker(content):
|
||||
break
|
||||
rest = content[1:].lstrip(" ")
|
||||
self.index += 1
|
||||
if not rest:
|
||||
next_line = self._peek_significant()
|
||||
if next_line is not None and next_line.indent > indent:
|
||||
result.append(self._parse_node(next_line.indent))
|
||||
else:
|
||||
result.append(None)
|
||||
continue
|
||||
if self._is_sequence_marker(rest):
|
||||
self._error(
|
||||
line,
|
||||
"不支持紧凑嵌套 sequence,请把内层 '-' 放到下一行",
|
||||
)
|
||||
if self._looks_like_mapping_entry(rest):
|
||||
mapping_indent = indent + 2
|
||||
item: dict[str, Any] = {}
|
||||
self._consume_mapping_entry(
|
||||
item,
|
||||
rest,
|
||||
mapping_indent=mapping_indent,
|
||||
line=line,
|
||||
)
|
||||
while True:
|
||||
self._skip_insignificant()
|
||||
continuation = self._peek_significant()
|
||||
if continuation is None or continuation.indent < mapping_indent:
|
||||
break
|
||||
if continuation.indent > mapping_indent:
|
||||
self._error(
|
||||
continuation,
|
||||
"sequence mapping 存在意外缩进",
|
||||
)
|
||||
continuation_content = self._without_comment(
|
||||
continuation.content
|
||||
).rstrip()
|
||||
if self._is_sequence_marker(continuation_content):
|
||||
self._error(
|
||||
continuation,
|
||||
"sequence mapping 中需要 key: value",
|
||||
)
|
||||
self.index += 1
|
||||
self._consume_mapping_entry(
|
||||
item,
|
||||
continuation_content,
|
||||
mapping_indent=mapping_indent,
|
||||
line=continuation,
|
||||
)
|
||||
result.append(item)
|
||||
continue
|
||||
if rest in {">", "|"}:
|
||||
result.append(self._parse_block_scalar(indent, rest, line))
|
||||
else:
|
||||
result.append(self._parse_inline_value(rest, line))
|
||||
next_line = self._peek_significant()
|
||||
if next_line is not None and next_line.indent > indent:
|
||||
self._error(next_line, "scalar sequence 项后存在意外缩进")
|
||||
return result
|
||||
|
||||
def _consume_mapping_entry(
|
||||
self,
|
||||
result: dict[str, Any],
|
||||
content: str,
|
||||
*,
|
||||
mapping_indent: int,
|
||||
line: _Line,
|
||||
) -> None:
|
||||
key_text, value_text = self._split_mapping_entry(content, line)
|
||||
key = self._parse_key(key_text, line)
|
||||
if key in result:
|
||||
self._error(line, f"mapping 存在重复键 {key!r}")
|
||||
|
||||
value_text = value_text.strip()
|
||||
if value_text in {">", "|"}:
|
||||
value = self._parse_block_scalar(mapping_indent, value_text, line)
|
||||
elif value_text:
|
||||
if value_text.startswith((">", "|")):
|
||||
self._error(
|
||||
line,
|
||||
"block scalar 只支持 '>' 或 '|',不支持 chomping/indent 指示符",
|
||||
)
|
||||
value = self._parse_inline_value(value_text, line)
|
||||
next_line = self._peek_significant()
|
||||
if next_line is not None and next_line.indent > mapping_indent:
|
||||
self._error(next_line, f"{key!r} 的 scalar 后存在意外缩进")
|
||||
else:
|
||||
next_line = self._peek_significant()
|
||||
if next_line is not None and next_line.indent > mapping_indent:
|
||||
value = self._parse_node(next_line.indent)
|
||||
else:
|
||||
value = None
|
||||
result[key] = value
|
||||
|
||||
def _parse_block_scalar(
|
||||
self,
|
||||
parent_indent: int,
|
||||
style: str,
|
||||
header: _Line,
|
||||
) -> str:
|
||||
probe = self.index
|
||||
while probe < len(self.lines) and not self.lines[probe].content.strip():
|
||||
probe += 1
|
||||
if probe >= len(self.lines) or self.lines[probe].indent <= parent_indent:
|
||||
return ""
|
||||
block_indent = self.lines[probe].indent
|
||||
if block_indent <= parent_indent:
|
||||
self._error(header, "block scalar 内容必须比键更深缩进")
|
||||
|
||||
values: list[str] = []
|
||||
while self.index < len(self.lines):
|
||||
line = self.lines[self.index]
|
||||
if not line.content.strip():
|
||||
values.append("")
|
||||
self.index += 1
|
||||
continue
|
||||
if line.indent < block_indent:
|
||||
break
|
||||
values.append(" " * (line.indent - block_indent) + line.content)
|
||||
self.index += 1
|
||||
|
||||
while values and values[-1] == "":
|
||||
values.pop()
|
||||
if not values:
|
||||
return ""
|
||||
if style == "|":
|
||||
return "\n".join(values) + "\n"
|
||||
|
||||
output = ""
|
||||
previous: str | None = None
|
||||
blank_count = 0
|
||||
for value in values:
|
||||
if value == "":
|
||||
blank_count += 1
|
||||
continue
|
||||
if previous is None:
|
||||
output = "\n" * blank_count + value
|
||||
elif blank_count:
|
||||
output += "\n" * blank_count + value
|
||||
elif previous.startswith(" ") or value.startswith(" "):
|
||||
output += "\n" + value
|
||||
else:
|
||||
output += " " + value
|
||||
previous = value
|
||||
blank_count = 0
|
||||
return output + "\n"
|
||||
|
||||
def _parse_key(self, text: str, line: _Line) -> str:
|
||||
text = text.strip()
|
||||
if not text:
|
||||
self._error(line, "mapping key 不能为空")
|
||||
if text[0] in {'"', "'"}:
|
||||
parsed = _FlowParser(text, line.number).parse_complete_value()
|
||||
if not isinstance(parsed, str):
|
||||
self._error(line, "mapping key 必须是字符串")
|
||||
return parsed
|
||||
if text == "<<":
|
||||
self._error(line, "ACK YAML 子集不支持 merge key '<<'")
|
||||
if text[0] in "-?:!&*%@`" or _PLAIN_KEY_FORBIDDEN_RE.search(text):
|
||||
self._error(line, f"不支持的 plain mapping key {text!r}")
|
||||
parsed = _plain_scalar(text, line.number)
|
||||
if not isinstance(parsed, str):
|
||||
self._error(line, "mapping key 必须是字符串,特殊标量请加引号")
|
||||
return text
|
||||
|
||||
def _parse_inline_value(self, text: str, line: _Line) -> Any:
|
||||
parser = _FlowParser(text, line.number)
|
||||
return parser.parse_complete_value()
|
||||
|
||||
def _split_mapping_entry(self, content: str, line: _Line) -> tuple[str, str]:
|
||||
quote: str | None = None
|
||||
escaped = False
|
||||
depth = 0
|
||||
index = 0
|
||||
while index < len(content):
|
||||
char = content[index]
|
||||
if quote == '"':
|
||||
if escaped:
|
||||
escaped = False
|
||||
elif char == "\\":
|
||||
escaped = True
|
||||
elif char == quote:
|
||||
quote = None
|
||||
index += 1
|
||||
continue
|
||||
if quote == "'":
|
||||
if char == "'":
|
||||
if index + 1 < len(content) and content[index + 1] == "'":
|
||||
index += 2
|
||||
continue
|
||||
quote = None
|
||||
index += 1
|
||||
continue
|
||||
if char in {'"', "'"}:
|
||||
quote = char
|
||||
elif char in "[{":
|
||||
depth += 1
|
||||
elif char in "]}":
|
||||
depth -= 1
|
||||
if depth < 0:
|
||||
self._error(line, "flow collection 括号不匹配")
|
||||
elif (
|
||||
char == ":"
|
||||
and depth == 0
|
||||
and (index + 1 == len(content) or content[index + 1].isspace())
|
||||
):
|
||||
return content[:index], content[index + 1 :]
|
||||
index += 1
|
||||
self._error(line, "mapping 项必须使用 'key: value'")
|
||||
|
||||
def _looks_like_mapping_entry(self, content: str) -> bool:
|
||||
try:
|
||||
self._split_mapping_entry(content, self.lines[self.index - 1])
|
||||
except YamlSubsetError:
|
||||
return False
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _is_sequence_marker(content: str) -> bool:
|
||||
return content == "-" or content.startswith("- ")
|
||||
|
||||
def _peek_significant(self) -> _Line | None:
|
||||
probe = self.index
|
||||
while probe < len(self.lines):
|
||||
line = self.lines[probe]
|
||||
if line.content.strip() and not line.content.lstrip().startswith("#"):
|
||||
return line
|
||||
probe += 1
|
||||
return None
|
||||
|
||||
def _skip_insignificant(self) -> None:
|
||||
while self.index < len(self.lines):
|
||||
content = self.lines[self.index].content
|
||||
if content.strip() and not content.lstrip().startswith("#"):
|
||||
break
|
||||
self.index += 1
|
||||
|
||||
def _without_comment(self, content: str) -> str:
|
||||
quote: str | None = None
|
||||
escaped = False
|
||||
index = 0
|
||||
while index < len(content):
|
||||
char = content[index]
|
||||
if quote == '"':
|
||||
if escaped:
|
||||
escaped = False
|
||||
elif char == "\\":
|
||||
escaped = True
|
||||
elif char == quote:
|
||||
quote = None
|
||||
elif quote == "'":
|
||||
if char == "'":
|
||||
if index + 1 < len(content) and content[index + 1] == "'":
|
||||
index += 1
|
||||
else:
|
||||
quote = None
|
||||
elif char in {'"', "'"}:
|
||||
quote = char
|
||||
elif char == "#" and (index == 0 or content[index - 1].isspace()):
|
||||
return content[:index]
|
||||
index += 1
|
||||
if quote is not None:
|
||||
raise YamlSubsetError("未结束的引号标量")
|
||||
return content
|
||||
|
||||
def _reject_document_syntax(self, content: str, line: _Line) -> None:
|
||||
if content in {"---", "..."} or content.startswith("%"):
|
||||
self._error(line, "ACK YAML 子集只支持单文档,不支持 directive/marker")
|
||||
if content.startswith(("!", "&", "*")):
|
||||
self._error(line, "ACK YAML 子集不支持 tag/anchor/alias")
|
||||
|
||||
@staticmethod
|
||||
def _error(line: _Line, message: str) -> None:
|
||||
raise YamlSubsetError(f"第 {line.number} 行: {message}")
|
||||
|
||||
|
||||
class _FlowParser:
|
||||
def __init__(self, text: str, line_number: int) -> None:
|
||||
self.text = text
|
||||
self.line_number = line_number
|
||||
self.index = 0
|
||||
|
||||
def parse_complete_value(self) -> Any:
|
||||
value = self._parse_value()
|
||||
self._skip_space()
|
||||
if self.index != len(self.text):
|
||||
self._error(f"标量后存在未支持内容: {self.text[self.index:]!r}")
|
||||
return value
|
||||
|
||||
def _parse_value(self) -> Any:
|
||||
self._skip_space()
|
||||
if self.index >= len(self.text):
|
||||
self._error("缺少标量")
|
||||
char = self.text[self.index]
|
||||
if char == "[":
|
||||
return self._parse_list()
|
||||
if char == "{":
|
||||
return self._parse_map()
|
||||
if char in {'"', "'"}:
|
||||
return self._parse_quoted()
|
||||
if char in "]},":
|
||||
self._error(f"意外字符 {char!r}")
|
||||
return self._parse_plain({",", "]", "}"})
|
||||
|
||||
def _parse_list(self) -> list[Any]:
|
||||
self.index += 1
|
||||
result: list[Any] = []
|
||||
self._skip_space()
|
||||
if self._consume("]"):
|
||||
return result
|
||||
while True:
|
||||
result.append(self._parse_value())
|
||||
self._skip_space()
|
||||
if self._consume("]"):
|
||||
return result
|
||||
if not self._consume(","):
|
||||
self._error("flow list 项之间必须用 ',' 分隔")
|
||||
self._skip_space()
|
||||
if self.index >= len(self.text) or self.text[self.index] == "]":
|
||||
self._error("flow list 不支持尾随逗号")
|
||||
|
||||
def _parse_map(self) -> dict[str, Any]:
|
||||
self.index += 1
|
||||
result: dict[str, Any] = {}
|
||||
self._skip_space()
|
||||
if self._consume("}"):
|
||||
return result
|
||||
while True:
|
||||
self._skip_space()
|
||||
if self.index >= len(self.text):
|
||||
self._error("flow mapping 未结束")
|
||||
if self.text[self.index] in {'"', "'"}:
|
||||
key = self._parse_quoted()
|
||||
else:
|
||||
key = self._parse_plain({":"}, convert=False)
|
||||
if not isinstance(key, str) or not key:
|
||||
self._error("flow mapping key 必须是非空字符串")
|
||||
if key == "<<":
|
||||
self._error("ACK YAML 子集不支持 merge key '<<'")
|
||||
self._skip_space()
|
||||
if not self._consume(":"):
|
||||
self._error("flow mapping key 后必须是 ':'")
|
||||
value = self._parse_value()
|
||||
if key in result:
|
||||
self._error(f"flow mapping 存在重复键 {key!r}")
|
||||
result[key] = value
|
||||
self._skip_space()
|
||||
if self._consume("}"):
|
||||
return result
|
||||
if not self._consume(","):
|
||||
self._error("flow mapping 项之间必须用 ',' 分隔")
|
||||
self._skip_space()
|
||||
if self.index >= len(self.text) or self.text[self.index] == "}":
|
||||
self._error("flow mapping 不支持尾随逗号")
|
||||
|
||||
def _parse_quoted(self) -> str:
|
||||
quote = self.text[self.index]
|
||||
self.index += 1
|
||||
result: list[str] = []
|
||||
while self.index < len(self.text):
|
||||
char = self.text[self.index]
|
||||
self.index += 1
|
||||
if char == quote:
|
||||
if quote == "'" and self.index < len(self.text) and self.text[
|
||||
self.index
|
||||
] == "'":
|
||||
result.append("'")
|
||||
self.index += 1
|
||||
continue
|
||||
return "".join(result)
|
||||
if quote == "'" or char != "\\":
|
||||
result.append(char)
|
||||
continue
|
||||
if self.index >= len(self.text):
|
||||
self._error("双引号标量以转义符结尾")
|
||||
escape = self.text[self.index]
|
||||
self.index += 1
|
||||
simple = {
|
||||
"0": "\0",
|
||||
"a": "\a",
|
||||
"b": "\b",
|
||||
"t": "\t",
|
||||
"n": "\n",
|
||||
"v": "\v",
|
||||
"f": "\f",
|
||||
"r": "\r",
|
||||
"e": "\x1b",
|
||||
" ": " ",
|
||||
'"': '"',
|
||||
"/": "/",
|
||||
"\\": "\\",
|
||||
}
|
||||
if escape in simple:
|
||||
result.append(simple[escape])
|
||||
continue
|
||||
widths = {"x": 2, "u": 4, "U": 8}
|
||||
if escape in widths:
|
||||
width = widths[escape]
|
||||
digits = self.text[self.index : self.index + width]
|
||||
if len(digits) != width or not re.fullmatch(
|
||||
rf"[0-9a-fA-F]{{{width}}}", digits
|
||||
):
|
||||
self._error(f"无效 Unicode 转义 \\{escape}{digits}")
|
||||
codepoint = int(digits, 16)
|
||||
try:
|
||||
result.append(chr(codepoint))
|
||||
except ValueError as exc:
|
||||
raise YamlSubsetError(
|
||||
f"第 {self.line_number} 行: 无效 Unicode 码点"
|
||||
) from exc
|
||||
self.index += width
|
||||
continue
|
||||
self._error(f"不支持的双引号转义 \\{escape}")
|
||||
self._error("引号标量未结束")
|
||||
|
||||
def _parse_plain(
|
||||
self,
|
||||
delimiters: set[str],
|
||||
*,
|
||||
convert: bool = True,
|
||||
) -> Any:
|
||||
start = self.index
|
||||
while self.index < len(self.text) and self.text[self.index] not in delimiters:
|
||||
self.index += 1
|
||||
token = self.text[start : self.index].strip()
|
||||
if not token:
|
||||
self._error("空 plain scalar")
|
||||
if "#" in token:
|
||||
self._error("flow collection 内的注释不受支持")
|
||||
if ": " in token:
|
||||
self._error("plain scalar 中的 ': ' 必须加引号")
|
||||
if token[0] in "!&*%@`?" or _ANCHOR_OR_ALIAS_RE.search(token):
|
||||
self._error("ACK YAML 子集不支持 tag/anchor/alias/directive")
|
||||
return _plain_scalar(token, self.line_number) if convert else token
|
||||
|
||||
def _skip_space(self) -> None:
|
||||
while self.index < len(self.text) and self.text[self.index] == " ":
|
||||
self.index += 1
|
||||
|
||||
def _consume(self, expected: str) -> bool:
|
||||
if self.index < len(self.text) and self.text[self.index] == expected:
|
||||
self.index += 1
|
||||
return True
|
||||
return False
|
||||
|
||||
def _error(self, message: str) -> None:
|
||||
raise YamlSubsetError(f"第 {self.line_number} 行: {message}")
|
||||
|
||||
|
||||
def _plain_scalar(token: str, line_number: int) -> Any:
|
||||
lowered = token.lower()
|
||||
if lowered in {"null", "~"}:
|
||||
return None
|
||||
if lowered in {"true", "yes", "on"}:
|
||||
return True
|
||||
if lowered in {"false", "no", "off"}:
|
||||
return False
|
||||
if _DECIMAL_INT_RE.fullmatch(token):
|
||||
return int(token, 10)
|
||||
if _AMBIGUOUS_NUMBER_RE.fullmatch(token):
|
||||
raise YamlSubsetError(
|
||||
f"第 {line_number} 行: ACK YAML 子集不支持该数字格式 {token!r},"
|
||||
"如需字符串请加引号"
|
||||
)
|
||||
return token
|
||||
@@ -0,0 +1,367 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "https://git.yumee.top/laily/skills/skills/ack/templates/knowledge.schema.json",
|
||||
"title": "ACK project knowledge guardrails",
|
||||
"description": "docs/ack/knowledge.yaml 的权威结构。知识只描述约束和验证引用,不保存可执行命令。",
|
||||
"type": "object",
|
||||
"required": ["version", "updatedAt", "project", "verificationRegistry", "entries"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"version": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"updatedAt": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"project": {
|
||||
"type": "object",
|
||||
"required": ["name"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
}
|
||||
}
|
||||
},
|
||||
"verificationRegistry": {
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
"pattern": "^[A-Za-z0-9][A-Za-z0-9._-]*$"
|
||||
},
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/verificationTarget"
|
||||
}
|
||||
},
|
||||
"entries": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/entry"
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"stableRef": {
|
||||
"type": "string",
|
||||
"pattern": "^K-[A-Z0-9][A-Z0-9-]*@[1-9][0-9]*$"
|
||||
},
|
||||
"stringSet": {
|
||||
"type": "array",
|
||||
"uniqueItems": true,
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
}
|
||||
},
|
||||
"scope": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"all",
|
||||
"components",
|
||||
"paths",
|
||||
"dependencies",
|
||||
"versions",
|
||||
"tags",
|
||||
"symbols",
|
||||
"errorSignatures"
|
||||
],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"all": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"components": {
|
||||
"$ref": "#/definitions/stringSet"
|
||||
},
|
||||
"paths": {
|
||||
"$ref": "#/definitions/stringSet"
|
||||
},
|
||||
"dependencies": {
|
||||
"$ref": "#/definitions/stringSet"
|
||||
},
|
||||
"versions": {
|
||||
"$ref": "#/definitions/stringSet"
|
||||
},
|
||||
"tags": {
|
||||
"$ref": "#/definitions/stringSet"
|
||||
},
|
||||
"symbols": {
|
||||
"$ref": "#/definitions/stringSet"
|
||||
},
|
||||
"errorSignatures": {
|
||||
"$ref": "#/definitions/stringSet"
|
||||
}
|
||||
}
|
||||
},
|
||||
"verification": {
|
||||
"type": "object",
|
||||
"required": ["ref", "expected"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"ref": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "^[A-Za-z0-9][A-Za-z0-9._-]*$"
|
||||
},
|
||||
"expected": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
}
|
||||
}
|
||||
},
|
||||
"verificationTarget": {
|
||||
"type": "object",
|
||||
"required": ["path", "args"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "^(?=.*\\S)[A-Za-z0-9][A-Za-z0-9._/-]*$",
|
||||
"description": "项目内可执行文件;运行时 cwd 为项目根,脚本不得依赖自身文件路径定位资源"
|
||||
},
|
||||
"args": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"provenance": {
|
||||
"type": "object",
|
||||
"required": ["taskId", "attemptId", "codeRef", "evidenceRef"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"taskId": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"attemptId": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"codeRef": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"evidenceRef": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entry": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"revision",
|
||||
"kind",
|
||||
"status",
|
||||
"title",
|
||||
"subject",
|
||||
"scope",
|
||||
"appliesWhen",
|
||||
"directive",
|
||||
"rationale",
|
||||
"verification",
|
||||
"provenance",
|
||||
"owner",
|
||||
"author",
|
||||
"reviewer",
|
||||
"approval",
|
||||
"createdAt",
|
||||
"lastValidatedAt",
|
||||
"reviewAfter",
|
||||
"temporary",
|
||||
"removalCondition",
|
||||
"statusReason",
|
||||
"supersedes",
|
||||
"conflictsWith"
|
||||
],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^K-[A-Z0-9][A-Z0-9-]*$"
|
||||
},
|
||||
"revision": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"enum": ["guardrail", "pitfall", "verification"]
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["active", "stale", "superseded", "archived"]
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"subject": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "^[a-z0-9][a-z0-9-]*$"
|
||||
},
|
||||
"scope": {
|
||||
"$ref": "#/definitions/scope"
|
||||
},
|
||||
"appliesWhen": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"directive": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"rationale": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"verification": {
|
||||
"$ref": "#/definitions/verification"
|
||||
},
|
||||
"provenance": {
|
||||
"$ref": "#/definitions/provenance"
|
||||
},
|
||||
"owner": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"author": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"reviewer": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"approval": {
|
||||
"oneOf": [
|
||||
{ "type": "null" },
|
||||
{ "$ref": "#/definitions/approval" }
|
||||
]
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"lastValidatedAt": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"reviewAfter": {
|
||||
"type": ["string", "null"],
|
||||
"format": "date-time"
|
||||
},
|
||||
"temporary": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"removalCondition": {
|
||||
"type": ["string", "null"],
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"statusReason": {
|
||||
"type": ["string", "null"],
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"supersedes": {
|
||||
"type": "array",
|
||||
"uniqueItems": true,
|
||||
"items": {
|
||||
"$ref": "#/definitions/stableRef"
|
||||
}
|
||||
},
|
||||
"conflictsWith": {
|
||||
"type": "array",
|
||||
"uniqueItems": true,
|
||||
"items": {
|
||||
"$ref": "#/definitions/stableRef"
|
||||
}
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"temporary": {
|
||||
"const": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"reviewAfter": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"removalCondition": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"status": {
|
||||
"enum": ["stale", "superseded", "archived"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"statusReason": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"approval": {
|
||||
"type": "object",
|
||||
"required": ["approvedBy", "approvedAt", "evidenceRef"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"approvedBy": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"approvedAt": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"evidenceRef": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
# 复制为 docs/ack/knowledge.yaml,替换占位符。结构见 templates/knowledge.schema.json。
|
||||
# Developer/Test 只能在任务证据中提出 candidate;只有 Coordinator 写入这里。
|
||||
version: 1
|
||||
updatedAt: "<YYYY-MM-DDTHH:mm:ss+TZ>"
|
||||
project:
|
||||
name: "<project_name>"
|
||||
# 检查 ID 映射到仓库内相对 path 和结构化 args;这里的内容不会被校验器执行。
|
||||
verificationRegistry: {}
|
||||
entries: []
|
||||
@@ -5,9 +5,11 @@
|
||||
>
|
||||
> **本文件是「项目覆盖层」,文件名可配置。** 默认放 `docs/ack/project.md`,
|
||||
> 不占用 `AGENTS.md`,避免与团队已有的 `AGENTS.md` 约定冲突。
|
||||
> 若希望 Agent 自动加载,可在项目 `AGENTS.md` 里加一行指向本文件,或直接把本文件命名为 `AGENTS.md`。
|
||||
> 若希望 Agent 自动加载,可由项目维护者自行在 `AGENTS.md` 中引用本文件;ACK
|
||||
> 不会自动修改 `AGENTS.md`、`CLAUDE.md` 或其它 Agent 指令文件。
|
||||
> 无论叫什么,都在 `tasks.yaml` 的 `project.overlayFile` 记录实际路径。
|
||||
> `docs/ack/` 只保存本项目的 `project.md` 与 `tasks.yaml`,不复制或链接 Skill。
|
||||
> `docs/ack/` 只保存本项目的 `project.md`、`tasks.yaml` 与 `knowledge.yaml`,
|
||||
> 不复制或链接 Skill。
|
||||
|
||||
## 项目概览
|
||||
|
||||
@@ -16,6 +18,7 @@
|
||||
- 运行命令:`<run_command>`
|
||||
- Base URL:`<base_url>`
|
||||
- 任务板:`docs/ack/tasks.yaml`
|
||||
- 项目知识:`docs/ack/knowledge.yaml`
|
||||
- 覆盖层文件:`<overlay_file_path>`(默认 `docs/ack/project.md`)
|
||||
|
||||
## 通用规范(由 ACK Skill 按需读取)
|
||||
@@ -48,6 +51,7 @@
|
||||
| `<shared_config_templates>` | Read-only | Read-only | R/W | 可提交配置模板 |
|
||||
| `<local_config_paths>` | Read-only | Read-only | Read-only | 本地私有配置 |
|
||||
| `tasks.yaml` | R/W | Read-only | Read-only | 只有 Coordinator 写 |
|
||||
| `knowledge.yaml` | R/W | Read-only | Read-only | 只有 Coordinator 写;Developer/Test 通过回报提名或验证 |
|
||||
|
||||
## 命令(项目覆盖层)
|
||||
|
||||
@@ -67,13 +71,28 @@ Test 黑盒复测:
|
||||
<browser_regression_command>
|
||||
```
|
||||
|
||||
任务板校验由 `/ack` 使用 Skill 自带的 `scripts/validate_tasks.py` 执行。
|
||||
知识项的 `verification.ref` 只能引用 `knowledge.yaml.verificationRegistry` 中已
|
||||
审查的检查入口。Registry 只保存仓库内相对 path 和结构化 args,不保存或执行自由
|
||||
shell 命令;新项目没有知识时保持空对象。Developer/Test 只能把 registry ID 交给
|
||||
Skill 的 `scripts/run_verification.py` 执行,不直接拼接 path/args。检查脚本从项目
|
||||
根 cwd 或 `ACK_PROJECT_ROOT` 定位资源,不能依赖自身文件路径。
|
||||
`ACK_PROJECT_ROOT` 是 runner 固定的根目录 fd 路径;仅用于日志的原始路径位于
|
||||
`ACK_PROJECT_ROOT_DISPLAY`。
|
||||
|
||||
项目状态校验由 `/ack` 使用 Skill 自带的 `scripts/validate_tasks.py` 和
|
||||
`scripts/validate_knowledge.py` 执行。
|
||||
|
||||
## 硬规则(其余见 references/)
|
||||
|
||||
- 三角色独立:Coordinator 只编排、Test 只验证、Developer 只实现(验证者 ≠ 实现者)。
|
||||
- 模型分层:Coordinator 用强模型且不亲自跑测试,Test/Developer 用中低模型,必要时升级(见 references/model-routing.md)。
|
||||
- `worker_done` 与复测报告都不等于完成。必须 Test 独立复测 + Coordinator 终检后才能 `verified`。
|
||||
- 只有 Coordinator 写 `tasks.yaml`;Developer 与 Test 都只读,通过消息回报。
|
||||
- 只有 Coordinator 写 `tasks.yaml` 和 `knowledge.yaml`;Developer 与 Test 都只读,
|
||||
通过消息回报。
|
||||
- Coordinator 只派发按 scope 命中并显式写入 `knowledgeRefs` 的 `active` 知识;
|
||||
`candidate` 不派发,知识库不全量注入。
|
||||
- Developer 回报 `knowledgeApplied` 与 `knowledgeCandidates`,Test 回报
|
||||
`knowledgeChecks`。关键约束应下沉为测试、lint、CI 或正式规范。
|
||||
- ACK 不自动修改 `AGENTS.md`、`CLAUDE.md` 或其它 Agent 指令文件。
|
||||
- 每个任务最多派发 3 轮,仍不过标记 `leftover` 并继续下一个。
|
||||
- 不提交或推送,除非用户明确要求。
|
||||
|
||||
@@ -23,13 +23,18 @@
|
||||
"required": ["name"],
|
||||
"additionalProperties": true,
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"name": { "type": "string", "minLength": 1, "pattern": "\\S" },
|
||||
"repoPath": { "type": "string" },
|
||||
"baseUrl": { "type": "string" },
|
||||
"devWorktree": { "type": "string" },
|
||||
"overlayFile": {
|
||||
"type": "string",
|
||||
"description": "项目覆盖层文件路径,默认 docs/ack/project.md,可自定义"
|
||||
},
|
||||
"knowledgeFile": {
|
||||
"type": "string",
|
||||
"const": "docs/ack/knowledge.yaml",
|
||||
"description": "项目知识护栏库的唯一权威路径"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -68,25 +73,155 @@
|
||||
"required": ["round", "result"],
|
||||
"additionalProperties": true,
|
||||
"properties": {
|
||||
"round": { "type": "integer", "minimum": 1 },
|
||||
"round": { "type": "integer", "minimum": 1, "maximum": 3 },
|
||||
"attemptId": {
|
||||
"type": "string",
|
||||
"pattern": "^[A-Za-z0-9][A-Za-z0-9._-]*-A[1-9][0-9]*$",
|
||||
"description": "稳定逻辑轮次 ID,应为 <task-id>-A<round>;旧轮次可缺省,但作为知识来源前必须补齐"
|
||||
},
|
||||
"result": { "type": "string", "enum": ["passed", "failed"] },
|
||||
"evidence": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"knowledgeRef": {
|
||||
"type": "string",
|
||||
"pattern": "^K-[A-Z0-9][A-Z0-9-]*@[1-9][0-9]*$"
|
||||
},
|
||||
"knowledgeCandidate": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"kind",
|
||||
"title",
|
||||
"claim",
|
||||
"scope",
|
||||
"appliesWhen",
|
||||
"directive",
|
||||
"rationale",
|
||||
"evidenceRefs"
|
||||
],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"enum": ["guardrail", "pitfall", "verification"]
|
||||
},
|
||||
"title": { "type": "string", "minLength": 1 },
|
||||
"claim": { "type": "string", "minLength": 1 },
|
||||
"scope": {
|
||||
"type": "object",
|
||||
"minProperties": 1,
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"all": { "type": "boolean" },
|
||||
"components": {
|
||||
"type": "array",
|
||||
"items": { "type": "string", "minLength": 1 },
|
||||
"uniqueItems": true
|
||||
},
|
||||
"paths": {
|
||||
"type": "array",
|
||||
"items": { "type": "string", "minLength": 1 },
|
||||
"uniqueItems": true
|
||||
},
|
||||
"dependencies": {
|
||||
"type": "array",
|
||||
"items": { "type": "string", "minLength": 1 },
|
||||
"uniqueItems": true
|
||||
},
|
||||
"versions": {
|
||||
"type": "array",
|
||||
"items": { "type": "string", "minLength": 1 },
|
||||
"uniqueItems": true
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": { "type": "string", "minLength": 1 },
|
||||
"uniqueItems": true
|
||||
},
|
||||
"symbols": {
|
||||
"type": "array",
|
||||
"items": { "type": "string", "minLength": 1 },
|
||||
"uniqueItems": true
|
||||
},
|
||||
"errorSignatures": {
|
||||
"type": "array",
|
||||
"items": { "type": "string", "minLength": 1 },
|
||||
"uniqueItems": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"appliesWhen": { "type": "string", "minLength": 1 },
|
||||
"directive": { "type": "string", "minLength": 1 },
|
||||
"rationale": { "type": "string", "minLength": 1 },
|
||||
"evidenceRefs": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": { "type": "string", "minLength": 1 },
|
||||
"uniqueItems": true
|
||||
},
|
||||
"proposedBy": { "type": "string" },
|
||||
"proposedAt": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"knowledgeApplication": {
|
||||
"type": "object",
|
||||
"required": ["ref", "result", "evidence"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"ref": { "$ref": "#/definitions/knowledgeRef" },
|
||||
"result": {
|
||||
"type": "string",
|
||||
"enum": ["applied", "not_applicable"]
|
||||
},
|
||||
"evidence": { "type": "string", "minLength": 1 }
|
||||
}
|
||||
},
|
||||
"knowledgeCheck": {
|
||||
"type": "object",
|
||||
"required": ["ref", "result", "evidence"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"ref": { "$ref": "#/definitions/knowledgeRef" },
|
||||
"result": {
|
||||
"type": "string",
|
||||
"enum": ["passed", "failed", "not_applicable"]
|
||||
},
|
||||
"evidence": { "type": "string", "minLength": 1 },
|
||||
"checkedBy": { "type": "string" },
|
||||
"checkedAt": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"task": {
|
||||
"type": "object",
|
||||
"required": ["id", "title", "status"],
|
||||
"additionalProperties": true,
|
||||
"properties": {
|
||||
"id": { "type": "string", "minLength": 1 },
|
||||
"id": { "type": "string", "minLength": 1, "pattern": "\\S" },
|
||||
"type": { "type": "string" },
|
||||
"title": { "type": "string", "minLength": 1 },
|
||||
"title": { "type": "string", "minLength": 1, "pattern": "\\S" },
|
||||
"priority": { "type": "string" },
|
||||
"status": { "$ref": "#/definitions/status" },
|
||||
"assignee": { "type": "string" },
|
||||
"component": { "type": "string" },
|
||||
"specRefs": { "type": "array", "items": { "type": "string" } },
|
||||
"testRefs": { "type": "array", "items": { "type": "string" } },
|
||||
"knowledgeRefs": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/definitions/knowledgeRef" },
|
||||
"uniqueItems": true
|
||||
},
|
||||
"knowledgeApplied": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/definitions/knowledgeApplication" }
|
||||
},
|
||||
"knowledgeCandidates": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/definitions/knowledgeCandidate" }
|
||||
},
|
||||
"knowledgeChecks": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/definitions/knowledgeCheck" }
|
||||
},
|
||||
"description": { "type": "string" },
|
||||
"stepsToReproduce": { "type": "array", "items": { "type": "string" } },
|
||||
"expected": { "type": "string" },
|
||||
@@ -124,6 +259,14 @@
|
||||
"then": {
|
||||
"properties": {
|
||||
"resolution": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"leftoverReason": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
}
|
||||
},
|
||||
"required": ["leftoverReason"]
|
||||
}
|
||||
},
|
||||
|
||||
@@ -9,6 +9,7 @@ project:
|
||||
baseUrl: "<base_url>"
|
||||
devWorktree: "<dev_worktree>"
|
||||
overlayFile: "docs/ack/project.md"
|
||||
knowledgeFile: "docs/ack/knowledge.yaml"
|
||||
|
||||
summary:
|
||||
verified: []
|
||||
@@ -28,6 +29,10 @@ tasks:
|
||||
- "<docs/spec.md#section>"
|
||||
testRefs:
|
||||
- "<tests/browser/cases/01-case.md>"
|
||||
knowledgeRefs: []
|
||||
knowledgeApplied: []
|
||||
knowledgeCandidates: []
|
||||
knowledgeChecks: []
|
||||
|
||||
description: >
|
||||
<What is wrong, in user-visible terms.>
|
||||
|
||||
Reference in New Issue
Block a user