feat: update

This commit is contained in:
2026-08-03 17:07:21 +08:00
parent 0954ad542a
commit b9c82b5520
15 changed files with 115 additions and 33 deletions
+4
View File
@@ -49,6 +49,10 @@ docs/ack/
不会在项目中复制或链接 ACK Skill。通用规范、模板和脚本始终从已安装的 Skill
目录读取。
ACK 从当前命令指定的 `--project-root/docs/ack/` 定位项目状态,不要求在
`tasks.yaml` 中持久化 `repoPath``devWorktree`。自动 worker 的实际工作目录仍由
`--worktree` 指定,并且必须命中绝对路径白名单 `allowedWorktrees`
## Skill 结构
```text
+4 -1
View File
@@ -53,6 +53,9 @@ description: >-
- 只写项目差异,不复制 `references/` 中的通用规范。
6. 完善 `docs/ack/tasks.yaml` 的项目信息。纯初始化且用户没有提供真实任务时,
删除模板示例任务并保留 `tasks: []`;不要虚构需求或缺陷。
项目状态固定从当前项目根的 `docs/ack/` 推导,不写入 `repoPath` 或 `devWorktree`
worker 的绝对路径只保存在 `project.orchestration.allowedWorktrees` 并在派发时通过
`--worktree` 显式选择。旧任务板中的 `repoPath`、`devWorktree` 仅兼容读取。
7. 检查 `docs/ack/knowledge.yaml`。新项目没有已验证的项目经验时保留
`verificationRegistry: {}` 与 `entries: []`,不从聊天、README 或单次失败中
猜测并激活知识。
@@ -90,7 +93,7 @@ description: >-
`kitVersion` 时仍可读取,但建议迁移为 `ackVersion`。`ackVersion` 必须是合法
SemVer;从 `0.10.0` 起 `project.orchestration` 与顶层 `workerReceipts` 必须同时
存在。
3. 查找未替换占位符,并核对项目路径、覆盖层路径、Developer 白盒命令、Test
3. 查找未替换占位符,并核对项目、覆盖层路径、Developer 白盒命令、Test
黑盒命令和 Base URL。
4. 使用 `<ack-skill-dir>/scripts/validate_tasks.py` 校验任务板,使用
`<ack-skill-dir>/scripts/validate_knowledge.py docs/ack/knowledge.yaml --tasks
+1 -1
View File
@@ -1 +1 @@
0.14.0
0.14.1
+1 -1
View File
@@ -1,6 +1,6 @@
# notes-web Agent 协作协议(示例,项目覆盖层)
> 本项目基于 ack v0.14.0
> 本项目基于 ack v0.14.1
> 通用规范由 `/ack` 从 Skill 自身的 `references/` 读取,本文件只填项目差异。
> 覆盖层文件放在 `docs/ack/project.md`,不占用 `AGENTS.md`
> ACK 不会自动修改 `AGENTS.md``CLAUDE.md` 或其它 Agent 指令文件。
+1 -3
View File
@@ -3,12 +3,10 @@
version: 1
updatedAt: "2026-07-06T09:40:00+08:00"
source: "Coordinator (PM) Agent"
ackVersion: "0.14.0"
ackVersion: "0.14.1"
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"
deliveryFile: "docs/ack/delivery.yaml"
@@ -24,6 +24,9 @@
- [ ] `project.orchestration` 使用受支持的 profileVersion,模型都命中项目
allowlist,默认 profile 与角色/档位一致。
- [ ] `allowedWorktrees` 只列出同一 Git common-dir 下已经核对的绝对 worktree。
- [ ] `tasks.yaml` 不需要保存 `repoPath``devWorktree`;项目状态从当前
`--project-root/docs/ack/` 推导,worker 路径由 `--worktree`
`allowedWorktrees` 共同约束。
## 路径权限
+5 -3
View File
@@ -83,11 +83,13 @@ docs/ack/
- 从 `0.11.0` 起的新项目初始化包含默认关闭的交付契约;旧项目不要求为了版本号升级
自动补交付配置。
- `updatedAt` 使用当前带时区时间。
- `project.name``repoPath``devWorktree``overlayFile``knowledgeFile` 使用
真实值。
- `project.name` 使用真实值;`overlayFile``knowledgeFile` 使用项目内相对路径。
ACK 从命令行 `--project-root` 下固定的 `docs/ack/` 布局解析项目状态,不把
`repoPath``devWorktree` 绝对路径写入任务板。旧任务板中的这两个字段仅兼容读取,
不再参与路径绑定。
- 新项目的 `project.deliveryFile` 固定为 `docs/ack/delivery.yaml`,并保留顶层
`deliveryRuns: []`。旧项目只有在采用交付能力时才补这两个字段。
- `project.orchestration.allowedWorktrees` 使用已核对的绝对 worktree;模型
- `project.orchestration.allowedWorktrees` 使用已核对的绝对 worker worktree;模型
allowlist、profiles 和 defaults 使用项目实际允许值。不要把完整启动命令、
`extraArgs``env` 或任意 executable 写进任务板。
- 非服务项目的 `baseUrl` 写为 `n/a`
+5
View File
@@ -127,6 +127,11 @@ python3 <ack-skill-dir>/scripts/launch_worker.py launch \
worker 实际工作的绝对路径,两者可以不同。后者仍必须属于同一项目并命中
`project.orchestration.allowedWorktrees`
项目状态文件固定从 `--project-root/docs/ack/` 解析;任务板不需要保存 `repoPath`
`devWorktree`。旧任务板中的这两个字段仅作兼容信息,launcher 不使用它们授权或定位。
`projectRoot`、任务板内容摘要和 worker worktree identity 都会进入 launch fingerprint
因此切换权威项目根、任务板内容或 worker 路径后必须重新生成并审阅 plan。
- `profile-hash` 读取 `tasks.yaml` 中的 profile,输出确定性 hash。
- `plan` 只校验并展示将使用的结构化计划,不创建终端。
- `launch` 是唯一允许创建 worker 的入口;它要求传入刚审阅的
+4 -10
View File
@@ -260,16 +260,6 @@ def load_authoritative_board(project_root_value: str) -> tuple[Path, dict]:
if errors:
formatted = "\n".join(f" - {error}" for error in dict.fromkeys(errors))
raise LaunchError(f"任务板未通过 worker 路由校验:\n{formatted}")
project = board.get("project")
repo_path = project.get("repoPath") if isinstance(project, dict) else None
if not isinstance(repo_path, str):
raise LaunchError("project.repoPath 必须配置为项目根绝对路径")
configured_root = canonical_directory(repo_path, "project.repoPath")
if configured_root != project_root:
raise LaunchError(
f"project.repoPath 与 --project-root 不一致: {configured_root}"
)
return project_root, board
@@ -641,6 +631,7 @@ def build_plan(
raise LaunchError("slot 必须是 1..99 的整数")
project_root, board = load_authoritative_board(project_root_value)
board_hash = canonical_sha256(board)
find_task(board, task_id)
project = board["project"]
orchestration = project.get("orchestration")
@@ -696,6 +687,8 @@ def build_plan(
{
"protocolVersion": PROTOCOL_VERSION,
"backend": "orca",
"projectRoot": str(project_root),
"boardHash": board_hash,
"profileId": profile_id,
"profileHash": current_profile_hash,
"createdFor": created_for,
@@ -715,6 +708,7 @@ def build_plan(
"protocolVersion": PROTOCOL_VERSION,
"backend": "orca",
"projectRoot": str(project_root),
"boardHash": board_hash,
"taskId": task_id,
"attemptId": attempt_id,
"role": role,
+10 -6
View File
@@ -240,20 +240,24 @@ def _validate_registry_target(
def _tasks_project_root(
tasks_data: dict[str, Any], tasks_path: Path | None
) -> Path | None:
inferred = infer_project_root(tasks_path) if tasks_path else None
if inferred is not None and inferred.is_dir():
return inferred
# Legacy task boards may still declare repoPath. It is only a fallback for
# non-standard layouts; docs/ack location is authoritative when available.
project = tasks_data.get("project")
repo_path = project.get("repoPath") if isinstance(project, dict) else None
if _nonempty(repo_path):
candidate = Path(repo_path).expanduser()
if not candidate.is_absolute():
inferred = infer_project_root(tasks_path) if tasks_path else None
if inferred is None:
return None
candidate = inferred / candidate
if not candidate.is_dir():
return None
return candidate.resolve(strict=True)
inferred = infer_project_root(tasks_path) if tasks_path else None
return inferred if inferred is not None and inferred.is_dir() else None
return None
def _unknown_keys(value: dict[str, Any], allowed: set[str], where: str) -> list[str]:
@@ -808,8 +812,8 @@ def _validate_knowledge_file_binding(
binding_root = project_root or declared_root
if binding_root is None or not binding_root.is_dir():
errors.append(
"[tasks] 无法确定现有项目根目录;请修正 project.repoPath "
"传入 --project-root"
"[tasks] 无法从 docs/ack 布局确定现有项目根目录;"
"传入 --project-root"
)
return errors
try:
@@ -1067,7 +1071,7 @@ def main(argv: list[str] | None = None) -> int:
parser.add_argument("--tasks", help="可选 tasks.yaml,用于跨文件引用校验")
parser.add_argument(
"--project-root",
help="可选项目根目录;默认从 tasks.project.repoPath 或 docs/ack 布局推断",
help="可选项目根目录;默认从 tasks.yaml 的 docs/ack 布局推断",
)
args = parser.parse_args(argv)
+10 -2
View File
@@ -25,9 +25,17 @@
"additionalProperties": true,
"properties": {
"name": { "type": "string", "minLength": 1, "pattern": "\\S" },
"repoPath": { "type": "string" },
"repoPath": {
"type": "string",
"deprecated": true,
"description": "Legacy informational field; ACK derives project files from --project-root/docs/ack"
},
"baseUrl": { "type": "string" },
"devWorktree": { "type": "string" },
"devWorktree": {
"type": "string",
"deprecated": true,
"description": "Legacy informational field; pass the actual worker path with --worktree"
},
"overlayFile": {
"type": "string",
"description": "项目覆盖层文件路径,默认 docs/ack/project.md,可自定义"
-2
View File
@@ -5,9 +5,7 @@ source: "Coordinator (PM) Agent"
ackVersion: "<接入时的 ack skill 版本>"
project:
name: "<project_name>"
repoPath: "<repo_path>"
baseUrl: "<base_url>"
devWorktree: "<dev_worktree>"
overlayFile: "docs/ack/project.md"
knowledgeFile: "docs/ack/knowledge.yaml"
deliveryFile: "docs/ack/delivery.yaml"
+2 -2
View File
@@ -612,7 +612,7 @@ class AckKnowledgeTests(unittest.TestCase):
)
self.assertFalse(any("knowledgeFile" in error for error in errors))
def test_cli_fails_closed_for_missing_repo_root_and_wrong_binding(self) -> None:
def test_cli_uses_tasks_layout_instead_of_stale_repo_path(self) -> None:
data = example_data()
task = {
"id": "BUG-002",
@@ -683,7 +683,7 @@ class AckKnowledgeTests(unittest.TestCase):
)
self.assertEqual(inferred.returncode, 1)
self.assertIn("project.repoPath", inferred.stderr)
self.assertNotIn("project.repoPath", inferred.stderr)
self.assertIn("与当前知识文件", inferred.stderr)
self.assertIn("symlink", inferred.stderr)
self.assertEqual(explicit.returncode, 1)
+54 -1
View File
@@ -65,12 +65,20 @@ def orchestration() -> dict:
def board(project_root: Path) -> dict:
return {
"version": 1,
"project": {
"name": "test-project",
"repoPath": str(project_root),
"orchestration": orchestration(),
},
"workerReceipts": [],
"tasks": [{"id": "TASK-001"}],
"tasks": [
{
"id": "TASK-001",
"title": "test task",
"status": "open",
}
],
}
@@ -90,6 +98,8 @@ def worktree_identity(path: Path) -> dict:
def plan_for(path: Path, executable: Path) -> dict:
profile = orchestration()["profiles"]["codex-dev-standard"]
task_board = board(path)
board_hash = worker_profiles.canonical_sha256(task_board)
identity = worktree_identity(path)
executable_metadata = executable.stat()
argv = worker_profiles.render_worker_argv(
@@ -121,6 +131,8 @@ def plan_for(path: Path, executable: Path) -> dict:
{
"protocolVersion": 1,
"backend": "orca",
"projectRoot": str(path),
"boardHash": board_hash,
"profileId": "codex-dev-standard",
"profileHash": profile_digest,
"createdFor": created_for,
@@ -134,6 +146,7 @@ def plan_for(path: Path, executable: Path) -> dict:
"protocolVersion": 1,
"backend": "orca",
"projectRoot": str(path),
"boardHash": board_hash,
"taskId": "TASK-001",
"attemptId": "TASK-001-A1",
"role": "developer",
@@ -362,6 +375,44 @@ class EnvironmentAndExecutableTests(unittest.TestCase):
class PlanTests(unittest.TestCase):
def test_authoritative_board_is_derived_from_project_root_without_repo_path(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
project = Path(temporary).resolve()
ack_dir = project / "docs" / "ack"
ack_dir.mkdir(parents=True)
task_board = board(project)
del task_board["project"]["repoPath"]
(ack_dir / "tasks.yaml").write_text(
json.dumps(task_board),
encoding="utf-8",
)
loaded_root, loaded_board = launch_worker.load_authoritative_board(
str(project)
)
self.assertEqual(loaded_root, project)
self.assertEqual(loaded_board, task_board)
def test_authoritative_board_ignores_legacy_repo_path(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
project = Path(temporary).resolve()
ack_dir = project / "docs" / "ack"
ack_dir.mkdir(parents=True)
task_board = board(project)
task_board["project"]["repoPath"] = "/legacy/other-worktree"
(ack_dir / "tasks.yaml").write_text(
json.dumps(task_board),
encoding="utf-8",
)
loaded_root, loaded_board = launch_worker.load_authoritative_board(
str(project)
)
self.assertEqual(loaded_root, project)
self.assertEqual(loaded_board, task_board)
def test_plan_uses_exact_renderer_and_binds_created_for(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
project = Path(temporary).resolve()
@@ -410,6 +461,8 @@ class PlanTests(unittest.TestCase):
{
"protocolVersion": 1,
"backend": "orca",
"projectRoot": str(project),
"boardHash": worker_profiles.canonical_sha256(task_board),
"profileId": plan["profileId"],
"profileHash": plan["profileHash"],
"createdFor": {
+11 -1
View File
@@ -67,7 +67,7 @@ class AckSkillContentTests(unittest.TestCase):
):
self.assertTrue((ack_dir / relative_path).is_file(), relative_path)
version = (ack_dir / "VERSION").read_text(encoding="utf-8").strip()
self.assertEqual(version, "0.14.0")
self.assertEqual(version, "0.14.1")
self.assertIn(
f'ackVersion: "{version}"',
(ack_dir / "examples" / "tasks.example.yaml").read_text(encoding="utf-8"),
@@ -84,6 +84,16 @@ class AckSkillContentTests(unittest.TestCase):
)
self.assertTrue((ack_dir / "references" / "feishu-bug-intake.md").is_file())
def test_task_template_derives_project_files_without_persisted_root_paths(self) -> None:
template = (
REPO_ROOT / "skills" / "ack" / "templates" / "tasks.template.yaml"
).read_text(encoding="utf-8")
self.assertNotIn("repoPath:", template)
self.assertNotIn("devWorktree:", template)
self.assertIn("allowedWorktrees:", template)
self.assertIn('knowledgeFile: "docs/ack/knowledge.yaml"', template)
if __name__ == "__main__":
unittest.main()