feat: update
This commit is contained in:
@@ -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
@@ -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
@@ -1 +1 @@
|
||||
0.14.0
|
||||
0.14.1
|
||||
|
||||
@@ -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 指令文件。
|
||||
|
||||
@@ -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` 共同约束。
|
||||
|
||||
## 路径权限
|
||||
|
||||
|
||||
@@ -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`。
|
||||
|
||||
@@ -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 的入口;它要求传入刚审阅的
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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,可自定义"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user