fix(ack): close Orca terminals after worker completion
This commit is contained in:
+1
-1
@@ -104,7 +104,7 @@ python3 <ack-skill-dir>/scripts/validate_agents.py .pouch/ack/agents.yaml
|
||||
|
||||
## 启动 worker
|
||||
|
||||
只使用 `scripts/launch_worker.py plan|launch`。不得直接 `orca terminal create --command`,不得拼接自由 command、argv、executable、env 或 cwd。读 [model-routing.md](references/model-routing.md)。先审阅 `plan.launchFingerprint`,再作为 `launch --expected-launch-fingerprint` 传入。派发文案用 [prompt-templates.md](references/prompt-templates.md)。派发后 terminal read 确认已开始;卡在审批、未回车或额度限制按环境失败处理。等待期间用 `scripts/worker_probe.py`,不盲等 `worker_done`。
|
||||
只使用 `scripts/launch_worker.py plan|launch`。不得直接 `orca terminal create --command`,不得拼接自由 command、argv、executable、env 或 cwd。读 [model-routing.md](references/model-routing.md)。先审阅 `plan.launchFingerprint`,再作为 `launch --expected-launch-fingerprint` 传入。派发文案用 [prompt-templates.md](references/prompt-templates.md)。派发后 terminal read 确认已开始;卡在审批、未回车或额度限制按环境失败处理。等待期间用 `scripts/worker_probe.py`,不盲等 `worker_done`。收到无错误的 `worker_done` / Test 复测结果后,按 [orca-adapter.md](references/orca-adapter.md) 用 `reclaim_workers.py --completed-task-id ... --completed-role ... --apply` 关闭对应 Orca terminal;失败、escalation 或环境异常不关闭。
|
||||
|
||||
## 交付配置维护
|
||||
|
||||
|
||||
@@ -284,7 +284,16 @@ orca orchestration send \
|
||||
--json
|
||||
```
|
||||
|
||||
收到 worker_done 后,Coordinator 写回 `fixed_by_dev`,再按上文派发给 Test 复测。
|
||||
收到无错误的 Developer `worker_done` 后,Coordinator 写回 `fixed_by_dev`,然后先回收
|
||||
Developer 对应的 Orca terminal,再按上文派发给 Test 复测:
|
||||
|
||||
```bash
|
||||
python3 <ack-skill-dir>/scripts/reclaim_workers.py \
|
||||
--project-root <project-root> \
|
||||
--completed-task-id <ack-task-id> --completed-role developer --apply
|
||||
```
|
||||
|
||||
只关闭该 task 的 Developer receipt 绑定 terminal;escalation、失败或环境异常时不要执行。
|
||||
|
||||
---
|
||||
|
||||
@@ -314,7 +323,18 @@ orca orchestration send \
|
||||
```
|
||||
|
||||
无 `retest_result` 类型时用 `--type worker_done`,靠 subject `retest round <n>`
|
||||
区分。收到复测结果后,Coordinator 按 `closed-loop.md` 做终检并回写 `tasks.yaml`;
|
||||
区分。收到无错误的 Test 复测结果后,先回收 Test 对应的 Orca terminal,再由
|
||||
Coordinator 按 `closed-loop.md` 做终检并回写 `tasks.yaml`:
|
||||
|
||||
```bash
|
||||
python3 <ack-skill-dir>/scripts/reclaim_workers.py \
|
||||
--project-root <project-root> \
|
||||
--completed-task-id <ack-task-id> --completed-role test --apply
|
||||
```
|
||||
|
||||
只有成功结果才能执行该命令;失败、环境异常或需要继续复验时保留 terminal。
|
||||
|
||||
收到复测结果后,Coordinator 按 `closed-loop.md` 做终检并回写 `tasks.yaml`;
|
||||
通过独立验证和 gate 的跨任务经验再由 Coordinator 写入 `knowledge.yaml`。任务通过
|
||||
写 `verified`;观察到产品信号失败写 `failed_retest`;环境无法完成则写
|
||||
`dispatch.environmentIncidents`,保持或恢复为 `fixed_by_dev`,不占复验轮次。
|
||||
@@ -323,8 +343,8 @@ orca orchestration send \
|
||||
|
||||
## 整轮结束时回收 worker
|
||||
|
||||
只有 Coordinator 已把本轮最终结论写入 `tasks.yaml` 后才开始回收。统一使用回收脚本,
|
||||
不要手工逐个 close:
|
||||
未在角色完成时回收的 worker,或需要整轮兜底回收的 worker,才在 Coordinator 把本轮
|
||||
最终结论写入 `tasks.yaml` 后执行整轮回收。统一使用回收脚本,不要手工逐个 close:
|
||||
|
||||
```bash
|
||||
# 第一步:dry-run 审阅决策(不关闭任何终端)
|
||||
|
||||
@@ -17,7 +17,9 @@ Default mode is dry-run: print decisions only. Pass ``--apply`` to actually
|
||||
show-verify and close. Closing is conservative: identity must match the
|
||||
receipt, the close receipt must be ok, and the handle must disappear from the
|
||||
live terminal list; anything uncertain stays retained and is reported instead
|
||||
of being retried.
|
||||
of being retried. A completed role can be reclaimed before the whole task is
|
||||
verified with ``--completed-task-id`` and ``--completed-role``; this is only
|
||||
valid after that role has reported success.
|
||||
|
||||
Output is a single JSON document:
|
||||
{"mode": "dry-run|apply", "handles": [{handle, decision, reason, tasks, closed}]}
|
||||
@@ -43,6 +45,10 @@ from launch_worker import ( # noqa: E402
|
||||
KEEP_STATUSES = frozenset(
|
||||
{"open", "dispatched", "fixed_by_dev", "retesting", "blocked", "failed_retest", "leftover"}
|
||||
)
|
||||
COMPLETED_ROLE_STATUSES = {
|
||||
"developer": frozenset({"fixed_by_dev", "retesting", "verified"}),
|
||||
"test": frozenset({"retesting", "verified"}),
|
||||
}
|
||||
|
||||
|
||||
def collect_handle_tasks(board: dict) -> dict[str, dict[str, object]]:
|
||||
@@ -115,10 +121,25 @@ def decide(handle: str, entry: dict[str, object]) -> tuple[str, str, list[str]]:
|
||||
|
||||
def close_terminal(orca: Path, handle: str) -> None:
|
||||
"""Show-verify identity, close the tab, then confirm it left the live list."""
|
||||
show = run_json(
|
||||
[str(orca), "terminal", "show", "--terminal", handle, "--json"],
|
||||
"terminal show",
|
||||
)
|
||||
try:
|
||||
show = run_json(
|
||||
[str(orca), "terminal", "show", "--terminal", handle, "--json"],
|
||||
"terminal show",
|
||||
)
|
||||
except LaunchError:
|
||||
# A role may already have been reclaimed before the final verified-task
|
||||
# sweep. Treat a handle absent from the live list as idempotent success;
|
||||
# retain other show failures for manual reconciliation.
|
||||
listing = run_json(
|
||||
[str(orca), "terminal", "list", "--json"],
|
||||
"terminal list",
|
||||
)
|
||||
terminals = listing.get("result", {}).get("terminals")
|
||||
if isinstance(terminals, list) and not any(
|
||||
isinstance(item, dict) and item.get("handle") == handle for item in terminals
|
||||
):
|
||||
return
|
||||
raise
|
||||
terminal = show.get("result", {}).get("terminal")
|
||||
if not isinstance(terminal, dict) or terminal.get("handle") != handle:
|
||||
raise LaunchError(f"terminal show 未返回匹配的 handle: {handle}")
|
||||
@@ -144,11 +165,70 @@ def close_terminal(orca: Path, handle: str) -> None:
|
||||
raise LaunchError(f"terminal close 后 handle 仍在 live list: {handle}")
|
||||
|
||||
|
||||
def completed_role_handle(board: dict, task_id: str, role: str) -> str:
|
||||
"""Resolve the receipt-bound terminal for a role that just succeeded."""
|
||||
|
||||
if role not in COMPLETED_ROLE_STATUSES:
|
||||
raise LaunchError(f"不支持的完成角色: {role}")
|
||||
tasks = board.get("tasks")
|
||||
tasks_by_id = (
|
||||
{task.get("id"): task for task in tasks if isinstance(task, dict)}
|
||||
if isinstance(tasks, list)
|
||||
else {}
|
||||
)
|
||||
task = tasks_by_id.get(task_id)
|
||||
if not isinstance(task, dict):
|
||||
raise LaunchError(f"找不到 ACK task: {task_id}")
|
||||
status = task.get("status")
|
||||
if status not in COMPLETED_ROLE_STATUSES[role]:
|
||||
raise LaunchError(
|
||||
f"{role} 完成回收要求任务处于 {sorted(COMPLETED_ROLE_STATUSES[role])},当前是 {status!r}"
|
||||
)
|
||||
if unresolved_incidents(task):
|
||||
raise LaunchError(f"task {task_id} 存在未解决环境事件,不能回收 {role} terminal")
|
||||
|
||||
dispatch = task.get("dispatch")
|
||||
role_dispatch = dispatch.get(role) if isinstance(dispatch, dict) else None
|
||||
receipt_id = role_dispatch.get("receiptId") if isinstance(role_dispatch, dict) else None
|
||||
if not isinstance(receipt_id, str) or not receipt_id:
|
||||
raise LaunchError(f"task {task_id} 缺少 {role} receiptId")
|
||||
|
||||
receipts = board.get("workerReceipts")
|
||||
receipt = next(
|
||||
(
|
||||
item
|
||||
for item in receipts
|
||||
if isinstance(item, dict) and item.get("id") == receipt_id
|
||||
),
|
||||
None,
|
||||
) if isinstance(receipts, list) else None
|
||||
if not isinstance(receipt, dict):
|
||||
raise LaunchError(f"找不到 {role} receipt: {receipt_id}")
|
||||
created_for = receipt.get("createdFor")
|
||||
binding = receipt.get("binding")
|
||||
if (
|
||||
not isinstance(created_for, dict)
|
||||
or created_for.get("taskId") != task_id
|
||||
or created_for.get("role") != role
|
||||
or not isinstance(binding, dict)
|
||||
or not isinstance(binding.get("handle"), str)
|
||||
or not binding.get("handle")
|
||||
):
|
||||
raise LaunchError(f"{role} receipt 与 task/role/handle 不匹配: {receipt_id}")
|
||||
return binding["handle"]
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--project-root", required=True, help="权威 tasks.yaml 所在项目根")
|
||||
parser.add_argument("--apply", action="store_true", help="真正关闭;默认只输出决策")
|
||||
parser.add_argument("--handle", help="只处理指定 handle(默认全部)")
|
||||
parser.add_argument("--completed-task-id", help="刚完成的 ACK task ID;与 --completed-role 一起使用")
|
||||
parser.add_argument(
|
||||
"--completed-role",
|
||||
choices=("developer", "test"),
|
||||
help="刚成功完成的角色;与 --completed-task-id 一起使用",
|
||||
)
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
try:
|
||||
@@ -157,6 +237,68 @@ def main(argv: list[str] | None = None) -> int:
|
||||
print(json.dumps({"mode": "dry-run" if not args.apply else "apply", "error": str(exc)}, ensure_ascii=False))
|
||||
return 2
|
||||
|
||||
if bool(args.completed_task_id) != bool(args.completed_role):
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"mode": "apply" if args.apply else "dry-run",
|
||||
"error": "--completed-task-id 与 --completed-role 必须同时提供",
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
)
|
||||
return 2
|
||||
if args.completed_task_id:
|
||||
if args.handle:
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"mode": "apply" if args.apply else "dry-run",
|
||||
"error": "完成角色回收不能同时使用 --handle",
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
)
|
||||
return 2
|
||||
try:
|
||||
handle = completed_role_handle(board, args.completed_task_id, args.completed_role)
|
||||
closed: bool | None = None
|
||||
decision = "close-completed-role"
|
||||
reason = f"{args.completed_role} reported success for task {args.completed_task_id}"
|
||||
if args.apply:
|
||||
close_terminal(resolve_executable("orca"), handle)
|
||||
closed = True
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"mode": "apply" if args.apply else "dry-run",
|
||||
"handles": [
|
||||
{
|
||||
"handle": handle,
|
||||
"decision": decision,
|
||||
"reason": reason,
|
||||
"tasks": [args.completed_task_id],
|
||||
"role": args.completed_role,
|
||||
"closed": closed,
|
||||
}
|
||||
],
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
)
|
||||
return 0
|
||||
except LaunchError as exc:
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"mode": "apply" if args.apply else "dry-run",
|
||||
"error": str(exc),
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
)
|
||||
return 2
|
||||
|
||||
handles = collect_handle_tasks(board)
|
||||
results: list[dict[str, object]] = []
|
||||
for handle in sorted(handles):
|
||||
|
||||
Reference in New Issue
Block a user