cowork-flow 1.8.0 → 1.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -1
- package/docs/direction.md +6 -4
- package/package.json +1 -1
- package/presets/claude-code/.claude-plugin/plugin.json +1 -1
- package/presets/codex/.codex-plugin/plugin.json +1 -1
- package/presets/kimi-code/.kimi-plugin/plugin.json +1 -1
- package/presets/qoder/.qoder-plugin/plugin.json +1 -1
- package/presets/zcode/.zcode-plugin/plugin.json +1 -1
- package/template/.cowork-flow/.version +1 -1
- package/template/.cowork-flow/scripts/adapters/cli/task.py +2 -0
- package/template/.cowork-flow/scripts/adapters/cli/task_lifecycle_commands.py +45 -0
- package/template/.cowork-flow/scripts/adapters/cli/task_next_runner.py +57 -0
- package/template/.cowork-flow/scripts/adapters/cli/task_parser.py +15 -1
- package/template/.cowork-flow/scripts/services/task_lifecycle.py +177 -2
- package/template/.cowork-flow/spec/contracts/index.md +1 -0
- package/template/.cowork-flow/spec/contracts/task-adopt.md +64 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,16 @@
|
|
|
5
5
|
### Added
|
|
6
6
|
|
|
7
7
|
|
|
8
|
+
## [1.8.1] - 2026-10-08
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **任务收养 `task next --run --adopt`(1.9.0 多执行者协作前置)**:为孤儿/滞留任务提供受支持的接管路径,替代手改 task.json meta。收养是独立动作(不与 review/complete/archive 链式同发):把 `in_progress`/`review` 状态、由另一执行者占有的任务重绑到收养者(`executor`),把评审基线**重置**到收养时 HEAD(原值记入 `meta.previousBaseline`),保留状态并写审计字段 `meta.previousExecutor` / `meta.previousBaseline` / `meta.adoptedAt`,结果 code `LIFECYCLE-EXECUTOR-ADOPTED`。误用 fail-closed(`LIFECYCLE-ADOPT-001`):planning/completed、无 executor、自我收养、process-fallback 身份、delegated 上下文、携带 create/plan 或批启动旗标全部拒绝且不改状态。start 路径"基线永不覆盖"不变量原样保留(既有测试零改动),收养是唯一经审计的显式重置路径;成功后 CLI 把会话绑定到该任务。契约见 `.cowork-flow/spec/contracts/task-adopt.md`。
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **`--allow-missing-evidence` 恢复独立解析**(1.8.0 遗留缺陷):该旗标此前被误注册为 `--allow-unchecked` 的解析别名(同一 `add_argument`、dest 相同),造成两个后果——AC 证据豁免(`LIFECYCLE-AC-002` 的可豁免路径)从 CLI 不可达(`cmd_complete` 读取的属性 argparse 从不创建,恒为 False);且传入 `--allow-missing-evidence` 会静默获得 spec-check 未检查豁免(语义错误、方向危险:本意只豁免 AC 证据,却放开了缺失/超时的 spec-check 命令)。现在两个旗标各自独立解析,parser 级与命令贯通级回归测试锁定(既有测试手工构造 Namespace 绕过 parser,正是此缺陷漏网原因)。
|
|
17
|
+
|
|
8
18
|
## [1.8.0] - 2026-10-08
|
|
9
19
|
|
|
10
20
|
### Added
|
|
@@ -296,7 +306,8 @@
|
|
|
296
306
|
|
|
297
307
|
此版本未保留本地条目正文,详情见 [GitHub Release](https://github.com/fishlikewater/cowork-flow/releases/tag/v0.0.47)。
|
|
298
308
|
|
|
299
|
-
[Unreleased]: https://github.com/fishlikewater/cowork-flow/compare/v1.8.
|
|
309
|
+
[Unreleased]: https://github.com/fishlikewater/cowork-flow/compare/v1.8.1...HEAD
|
|
310
|
+
[1.8.1]: https://github.com/fishlikewater/cowork-flow/compare/v1.8.0...v1.8.1
|
|
300
311
|
[1.8.0]: https://github.com/fishlikewater/cowork-flow/compare/v1.7.3...v1.8.0
|
|
301
312
|
[1.7.3]: https://github.com/fishlikewater/cowork-flow/compare/v1.7.2...v1.7.3
|
|
302
313
|
[1.7.2]: https://github.com/fishlikewater/cowork-flow/compare/v1.7.1...v1.7.2
|
package/docs/direction.md
CHANGED
|
@@ -16,10 +16,11 @@
|
|
|
16
16
|
| 本规划文档落库 | ✅ 完成 | 08-27-direction-doc | — |
|
|
17
17
|
| 1.5→1.7 宿主广度 + 发布链(七宿主) | ✅ 完成 | CHANGELOG 1.5.0-1.7.2 | — |
|
|
18
18
|
| 1.7.3 维护:ZCode shim .mjs + 项目根查找修复 | ✅ 完成 | 10-04-zcode-shim-mjs(d0716a9) | — |
|
|
19
|
-
| 1.8.0 立项:证据化完成 |
|
|
20
|
-
| 1.8.0 主线实现:证据化完成 | ✅ 完成 | 10-05-evidence-completion(baa386d,CI test + windows-core
|
|
21
|
-
| 1.8.x 收尾:Skill 证据补课 |
|
|
22
|
-
| 1.9.0 前置:task adopt 命令 |
|
|
19
|
+
| 1.8.0 立项:证据化完成 | ✅ 完成 | 10-05-direction-1-8-0 / 10-05-evidence-completion | — |
|
|
20
|
+
| 1.8.0 主线实现:证据化完成 | ✅ 完成 | 10-05-evidence-completion(baa386d,CI test + windows-core 全绿);已随 1.8.0 发布(cfe1ac1) | — |
|
|
21
|
+
| 1.8.x 收尾:Skill 证据补课 + spec-check 采纳闭环 | ✅ 完成 | 10-06-skill-evidence-training(09937d0)、10-08 spec-check 闭环(08f9d9b) | — |
|
|
22
|
+
| 1.9.0 前置:task adopt 命令 | ✅ 完成 | 10-06-task-adopt-command(e6e5132):`--adopt` 重绑执行者 + 基线重置 + 审计;契约 task-adopt.md | 1.9.0 主线(多执行者协作)立项 |
|
|
23
|
+
| 1.8.1 补丁:`--allow-missing-evidence` 解析修复 + adopt | 🔄 本次发布 | 10-08-fix-evidence-flag-parser(dc594a4);tag v1.8.1 → CI 发布 npm | 确认 CI 全绿与 npm latest=1.8.1 |
|
|
23
24
|
|
|
24
25
|
**当前结论**:方向路线图阶段 0-3 全部落地。项目当前形态:「多主机 Agent 协作的运行时上下文与协作事实层」——事实视图(CLI + MCP)、结构化注入、跨宿主一致性治理、执行者归属与证据位均已就绪并有测试锁定。后续演进见「不做什么」边界与观察期事项。
|
|
25
26
|
|
|
@@ -177,3 +178,4 @@
|
|
|
177
178
|
- 2026-08-29:MCP 全局入口补充——npm CLI 新增 `cowork-flow mcp-state` 透传命令(cwd 向上定位项目运行时、stdio inherit、退出码透传),MCP 客户端全局注册一次即可服务所有项目,消除逐项目 `.mcp.json` 配置(9a1fa86)。
|
|
178
179
|
- 2026-08-29:后续方向 P0-P3 执行完毕。**P0**:1.1.0 发布——dev→master 合并、CHANGELOG 1.1.0 段(含 1.0.0 载体错位注明)、tag v1.1.0 与 GitHub Release 创建;publish CI 首跑曝光 node:full 存量问题(dsh-hook 依赖 gitignored 运行时、Windows 无法 exec shebang 脚本、test runner IPC 崩溃)并全部修复——双平台门禁现已全绿。**发布确认:npm registry `cowork-flow@1.1.0` 已上线(dist-tag latest),AC-004 闭合,发版任务归档。** **P1**:`docs/mcp-client-setup.md`(codex/opencode/claude-code/zcode 真实注册格式)+ `test/mcp-client-matrix.test.js` 客户端等效启动验证。**P1'**:publish.yml 链路实测核对并写入 README 发布节。**P2**:flaky 组合复跑 5 轮未复现(记录为单次现象);doctor 会话卫生无残留。**P3**:观察期边界维持——MCP 写工具默认拒绝,AGNTCY 等协议收敛后按薄壳接入。- 2026-10-05:2026-10 修订落库(任务 10-05-direction-1-8-0)。1.7.3 维护完成(10-04-zcode-shim-mjs,d0716a9:ZCode shim 改 .mjs、findWorkflowRoot off-by-one 修复);1.8.0 主线定为**证据化完成**,主线任务 10-05-evidence-completion 立项(planning 就绪待启动);候选 B(agent-teams 就绪)列为 1.9.0 候选主线;文档英文化列为待决项,待维护者拍板。方向文档自本次起恢复版本控制(087ef72 untrack 后首次入库)。
|
|
179
180
|
- 2026-10-06:10.5 增补落库(任务 10-06-direction-post-1-8-0)。1.8.0 主线实现完成(10-05-evidence-completion,baa386d,CI 双平台全绿);立项 10-06-skill-evidence-training(1.8.0 收尾)与 10-06-task-adopt-command(1.9.0 前置);1.8.0 发布判据 = CI + 真实宿主冒烟 + Skill 补课。
|
|
181
|
+
- 2026-10-08:1.8.0 已发布(cfe1ac1,tag v1.8.0,CI publish 全绿,npm latest=1.8.0)。发布后排查发现 `--allow-missing-evidence` 被误注册为 `--allow-unchecked` 解析别名的遗留缺陷(AC 证据豁免从 CLI 不可达 + 误授 spec-check 豁免),修复入库(10-08-fix-evidence-flag-parser,dc594a4,parser 级与命令贯通级回归测试锁定)。1.9.0 前置 task adopt 完成(10-06-task-adopt-command,e6e5132):`task next --run --adopt` 独立动作——重绑执行者、评审基线重置到收养时 HEAD(审计保留原值)、状态不变、误用 fail-closed(`LIFECYCLE-ADOPT-001`);契约 `spec/contracts/task-adopt.md`,含与 `--takeover` 的边界。本次发布 1.8.1 同时携带两项交付(CHANGELOG 分列 Added/Fixed);1.9.0 主线(多执行者协作就绪)待立项。
|
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.8.
|
|
1
|
+
1.8.1
|
|
@@ -38,6 +38,7 @@ from adapters.cli.task_context_commands import (
|
|
|
38
38
|
from adapters.cli.task_create_command import cmd_create, ensure_tasks_dir
|
|
39
39
|
from adapters.cli import task_navigation
|
|
40
40
|
from adapters.cli.task_lifecycle_commands import (
|
|
41
|
+
cmd_adopt,
|
|
41
42
|
cmd_complete,
|
|
42
43
|
cmd_current,
|
|
43
44
|
cmd_finish,
|
|
@@ -152,6 +153,7 @@ def cmd_next(args: argparse.Namespace) -> int:
|
|
|
152
153
|
review=cmd_review,
|
|
153
154
|
complete=cmd_complete,
|
|
154
155
|
archive=cmd_archive,
|
|
156
|
+
adopt=cmd_adopt,
|
|
155
157
|
),
|
|
156
158
|
)
|
|
157
159
|
return task_navigation.cmd_next(args)
|
|
@@ -30,6 +30,7 @@ from runtime.session_state import (
|
|
|
30
30
|
PROVENANCE_PROCESS_FALLBACK,
|
|
31
31
|
clear_active_task,
|
|
32
32
|
get_active_task,
|
|
33
|
+
set_active_task,
|
|
33
34
|
)
|
|
34
35
|
|
|
35
36
|
|
|
@@ -284,6 +285,50 @@ def cmd_start(args: argparse.Namespace) -> int:
|
|
|
284
285
|
return 0
|
|
285
286
|
|
|
286
287
|
|
|
288
|
+
def cmd_adopt(args: argparse.Namespace) -> int:
|
|
289
|
+
"""Adopt an active task from another executor."""
|
|
290
|
+
repo_root = get_repo_root()
|
|
291
|
+
execution_context = execution_context_from_namespace(args)
|
|
292
|
+
task_input = getattr(args, "dir", None)
|
|
293
|
+
if not task_input:
|
|
294
|
+
print(
|
|
295
|
+
colored("Error: task directory or name required", Colors.RED),
|
|
296
|
+
file=sys.stderr,
|
|
297
|
+
)
|
|
298
|
+
return 1
|
|
299
|
+
|
|
300
|
+
full_path = _resolve_start_task(task_input, repo_root)
|
|
301
|
+
if full_path is None:
|
|
302
|
+
return 1
|
|
303
|
+
|
|
304
|
+
service = TaskLifecycleService(repo_root)
|
|
305
|
+
result = service.adopt(
|
|
306
|
+
full_path,
|
|
307
|
+
executor=getattr(args, "executor", None),
|
|
308
|
+
execution_context=execution_context,
|
|
309
|
+
)
|
|
310
|
+
if not result.ok:
|
|
311
|
+
if result.code == "LIFECYCLE-ADOPT-001":
|
|
312
|
+
print(
|
|
313
|
+
colored("Error: task adoption blocked", Colors.RED),
|
|
314
|
+
file=sys.stderr,
|
|
315
|
+
)
|
|
316
|
+
for blocker in result.blockers:
|
|
317
|
+
print(f" - {blocker}", file=sys.stderr)
|
|
318
|
+
return 1
|
|
319
|
+
if result.title:
|
|
320
|
+
return _report_lifecycle_preflight(result)
|
|
321
|
+
if result.repository_error is not None:
|
|
322
|
+
return _report_lifecycle_repository_error(result)
|
|
323
|
+
return 1
|
|
324
|
+
|
|
325
|
+
task_path = _display_task_path(repo_root, full_path)
|
|
326
|
+
set_active_task(repo_root, task_path)
|
|
327
|
+
print(colored(f"[OK] Task adopted: {task_path}", Colors.GREEN))
|
|
328
|
+
print(f"Next: ./.cowork-flow/run task next {task_path} --run")
|
|
329
|
+
return 0
|
|
330
|
+
|
|
331
|
+
|
|
287
332
|
def cmd_review(args: argparse.Namespace) -> int:
|
|
288
333
|
"""Mark a task ready for check."""
|
|
289
334
|
repo_root = get_repo_root()
|
|
@@ -18,6 +18,7 @@ from runtime.session_state import (
|
|
|
18
18
|
get_active_task,
|
|
19
19
|
)
|
|
20
20
|
from services.plan_binding import PlanBindingError, bind_task_plan
|
|
21
|
+
from services.task_lifecycle import ADOPTABLE_STATUSES
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
LifecycleCommand = Callable[[argparse.Namespace], int]
|
|
@@ -30,6 +31,7 @@ class NextActionHandlers:
|
|
|
30
31
|
review: LifecycleCommand
|
|
31
32
|
complete: LifecycleCommand
|
|
32
33
|
archive: LifecycleCommand
|
|
34
|
+
adopt: LifecycleCommand
|
|
33
35
|
|
|
34
36
|
|
|
35
37
|
def namespace_with(args: argparse.Namespace, **overrides) -> argparse.Namespace:
|
|
@@ -270,7 +272,62 @@ def _run_plan_bind_action(args: argparse.Namespace, task_path: str) -> int:
|
|
|
270
272
|
return 0
|
|
271
273
|
|
|
272
274
|
|
|
275
|
+
def _run_adopt_action(
|
|
276
|
+
args: argparse.Namespace,
|
|
277
|
+
handlers: NextActionHandlers,
|
|
278
|
+
) -> int:
|
|
279
|
+
"""`--adopt` is its own action: it repairs ownership and the review
|
|
280
|
+
baseline without running any lifecycle transition."""
|
|
281
|
+
repo_root = get_repo_root()
|
|
282
|
+
if _create_input_names(args) or getattr(args, "from_plan", None):
|
|
283
|
+
print(
|
|
284
|
+
colored(
|
|
285
|
+
"Error: --adopt takes over an existing task; create/plan "
|
|
286
|
+
"inputs cannot run with it",
|
|
287
|
+
Colors.RED,
|
|
288
|
+
),
|
|
289
|
+
file=sys.stderr,
|
|
290
|
+
)
|
|
291
|
+
return 1
|
|
292
|
+
if getattr(args, "auto", False) or getattr(args, "approved", False):
|
|
293
|
+
print(
|
|
294
|
+
colored(
|
|
295
|
+
"Error: --adopt does not run batch start; adopt first, then "
|
|
296
|
+
"re-run with --auto --approved",
|
|
297
|
+
Colors.RED,
|
|
298
|
+
),
|
|
299
|
+
file=sys.stderr,
|
|
300
|
+
)
|
|
301
|
+
return 1
|
|
302
|
+
task_dir, task_path, _active_target = _next_target_for_run(args, repo_root)
|
|
303
|
+
if task_dir is None or task_path is None:
|
|
304
|
+
blockers = _fallback_binding_blockers(args, repo_root)
|
|
305
|
+
for blocker in blockers or ["no task target for --adopt; pass a task dir"]:
|
|
306
|
+
print(colored(f"Error: {blocker}", Colors.RED), file=sys.stderr)
|
|
307
|
+
return 1
|
|
308
|
+
if not task_dir.is_dir():
|
|
309
|
+
print(
|
|
310
|
+
colored(f"Error: task directory not found: {task_path}", Colors.RED),
|
|
311
|
+
file=sys.stderr,
|
|
312
|
+
)
|
|
313
|
+
return 1
|
|
314
|
+
status = task_navigation._status(repo_root, task_dir)
|
|
315
|
+
if status not in ADOPTABLE_STATUSES:
|
|
316
|
+
print(
|
|
317
|
+
colored(
|
|
318
|
+
"Error: --adopt only applies to an active task "
|
|
319
|
+
f"({'/'.join(ADOPTABLE_STATUSES)}); current status: {status}",
|
|
320
|
+
Colors.RED,
|
|
321
|
+
),
|
|
322
|
+
file=sys.stderr,
|
|
323
|
+
)
|
|
324
|
+
return 1
|
|
325
|
+
return handlers.adopt(namespace_with(args, dir=task_path))
|
|
326
|
+
|
|
327
|
+
|
|
273
328
|
def run_next_action(args: argparse.Namespace, handlers: NextActionHandlers) -> int:
|
|
329
|
+
if bool(getattr(args, "adopt", False)):
|
|
330
|
+
return _run_adopt_action(args, handlers)
|
|
274
331
|
action, error_code = _validated_next_action(args)
|
|
275
332
|
if action is None:
|
|
276
333
|
return error_code
|
|
@@ -31,6 +31,8 @@ Action inputs for `next --run`:
|
|
|
31
31
|
--parent <dir> Optional parent task directory for create_task
|
|
32
32
|
--from-plan <path> Plan path for create_task or planning-phase rebind
|
|
33
33
|
--auto --approved Optional batch start flags for start_task
|
|
34
|
+
--adopt Adopt an active task from another executor (rebind
|
|
35
|
+
executor + reset review baseline; status unchanged)
|
|
34
36
|
--commit Optional archive auto-commit flag for archive_task
|
|
35
37
|
|
|
36
38
|
Examples:
|
|
@@ -86,12 +88,18 @@ def _add_next_output_options(parser: argparse.ArgumentParser) -> None:
|
|
|
86
88
|
)
|
|
87
89
|
parser.add_argument(
|
|
88
90
|
"--allow-unchecked",
|
|
89
|
-
"--allow-missing-evidence",
|
|
90
91
|
dest="allow_unchecked",
|
|
91
92
|
action="store_true",
|
|
92
93
|
help="With task completion: allow unchecked spec-check commands "
|
|
93
94
|
"(missing/timeout) and record the exemption in task.json meta",
|
|
94
95
|
)
|
|
96
|
+
parser.add_argument(
|
|
97
|
+
"--allow-missing-evidence",
|
|
98
|
+
dest="allow_missing_evidence",
|
|
99
|
+
action="store_true",
|
|
100
|
+
help="With task completion: allow checked acceptance criteria "
|
|
101
|
+
"without evidence and record the exemption in task.json meta",
|
|
102
|
+
)
|
|
95
103
|
parser.add_argument(
|
|
96
104
|
"--list",
|
|
97
105
|
dest="list_tasks",
|
|
@@ -157,6 +165,12 @@ def _add_next_runtime_inputs(parser: argparse.ArgumentParser) -> None:
|
|
|
157
165
|
action="store_true",
|
|
158
166
|
help="Take the task over from its current executor on start_task",
|
|
159
167
|
)
|
|
168
|
+
parser.add_argument(
|
|
169
|
+
"--adopt",
|
|
170
|
+
action="store_true",
|
|
171
|
+
help="Adopt an active task from another executor: rebind executor, "
|
|
172
|
+
"reset the review baseline to HEAD, keep status, record audit fields",
|
|
173
|
+
)
|
|
160
174
|
|
|
161
175
|
|
|
162
176
|
def _add_next_command(subparsers: argparse._SubParsersAction) -> None:
|
|
@@ -60,6 +60,10 @@ COMPLETE_STAGE = LifecycleStage(
|
|
|
60
60
|
records_completion_date=True,
|
|
61
61
|
)
|
|
62
62
|
|
|
63
|
+
# Adoption repairs ownership on an unfinished task; finished or unstarted
|
|
64
|
+
# tasks have nothing to hand over.
|
|
65
|
+
ADOPTABLE_STATUSES = ("in_progress", "review")
|
|
66
|
+
|
|
63
67
|
|
|
64
68
|
@dataclass(frozen=True)
|
|
65
69
|
class LifecyclePreflightFailure:
|
|
@@ -139,6 +143,73 @@ class TaskLifecycleService:
|
|
|
139
143
|
execution_context=execution_context,
|
|
140
144
|
)
|
|
141
145
|
|
|
146
|
+
def adopt(
|
|
147
|
+
self,
|
|
148
|
+
task: str | Path,
|
|
149
|
+
*,
|
|
150
|
+
executor: str | None = None,
|
|
151
|
+
execution_context: object | None = None,
|
|
152
|
+
) -> LifecycleResult:
|
|
153
|
+
"""Take over an active task owned by another executor: rebind the
|
|
154
|
+
executor, reset the review baseline to HEAD, keep status, record
|
|
155
|
+
audit fields. Adoption repairs ownership; it is not a transition."""
|
|
156
|
+
task_dir = self.repository.resolve(task)
|
|
157
|
+
task_data_or_failure = self._load_transition_task(START_STAGE, task_dir)
|
|
158
|
+
if isinstance(task_data_or_failure, LifecycleResult):
|
|
159
|
+
return task_data_or_failure
|
|
160
|
+
task_data = task_data_or_failure
|
|
161
|
+
|
|
162
|
+
resolved, failure = self._adopt_gate(
|
|
163
|
+
START_STAGE,
|
|
164
|
+
task_dir,
|
|
165
|
+
task_data,
|
|
166
|
+
executor,
|
|
167
|
+
execution_context,
|
|
168
|
+
)
|
|
169
|
+
if failure is not None:
|
|
170
|
+
return failure
|
|
171
|
+
current_task, failure = self._takeover_snapshot(
|
|
172
|
+
START_STAGE, task_dir, task_data
|
|
173
|
+
)
|
|
174
|
+
if failure is not None:
|
|
175
|
+
return failure
|
|
176
|
+
|
|
177
|
+
current = str(task_data.get("executor")).strip()
|
|
178
|
+
meta = dict(task_data.get("meta") or {})
|
|
179
|
+
adoption = {
|
|
180
|
+
"previousExecutor": current,
|
|
181
|
+
"previousBaseline": meta.get("baselineCommit"),
|
|
182
|
+
"adoptedAt": datetime.now(timezone.utc).strftime(
|
|
183
|
+
"%Y-%m-%dT%H:%M:%SZ"
|
|
184
|
+
),
|
|
185
|
+
# Reset, not preserve: the adopter's review window starts at the
|
|
186
|
+
# adoption HEAD. No HEAD (no git) degrades to status-only.
|
|
187
|
+
"baselineCommit": current_head(self.repo_root),
|
|
188
|
+
}
|
|
189
|
+
failure = self._write_takeover(
|
|
190
|
+
START_STAGE,
|
|
191
|
+
task_dir,
|
|
192
|
+
task_data,
|
|
193
|
+
resolved,
|
|
194
|
+
current_task,
|
|
195
|
+
adoption=adoption,
|
|
196
|
+
operation_tag="adopt",
|
|
197
|
+
)
|
|
198
|
+
if failure is not None:
|
|
199
|
+
return failure
|
|
200
|
+
return LifecycleResult(
|
|
201
|
+
ok=True,
|
|
202
|
+
code="LIFECYCLE-EXECUTOR-ADOPTED",
|
|
203
|
+
stage=START_STAGE,
|
|
204
|
+
task_dir=task_dir,
|
|
205
|
+
active_task_path=self._display_task_path(task_dir),
|
|
206
|
+
transition=LifecycleTransition(
|
|
207
|
+
previous_status=task_data.get("status"),
|
|
208
|
+
next_status=task_data.get("status"),
|
|
209
|
+
changed=False,
|
|
210
|
+
),
|
|
211
|
+
)
|
|
212
|
+
|
|
142
213
|
def review(
|
|
143
214
|
self,
|
|
144
215
|
task: str | Path,
|
|
@@ -717,6 +788,103 @@ class TaskLifecycleService:
|
|
|
717
788
|
return None
|
|
718
789
|
return key or None
|
|
719
790
|
|
|
791
|
+
def _adopt_gate(
|
|
792
|
+
self,
|
|
793
|
+
stage: LifecycleStage,
|
|
794
|
+
task_dir: Path,
|
|
795
|
+
task_data: dict,
|
|
796
|
+
executor: str | None,
|
|
797
|
+
execution_context: object | None,
|
|
798
|
+
) -> tuple[str | None, LifecycleResult | None]:
|
|
799
|
+
"""Fail-closed adoption gate: only an active task owned by another
|
|
800
|
+
executor can be adopted, and only by a real (non-shared) identity."""
|
|
801
|
+
if self._delegated_mutation_request(execution_context):
|
|
802
|
+
return None, self._failure(
|
|
803
|
+
stage,
|
|
804
|
+
task_dir,
|
|
805
|
+
"TASK-EXECUTION-001",
|
|
806
|
+
title="Delegated execution cannot mutate task lifecycle",
|
|
807
|
+
blockers=(
|
|
808
|
+
"delegated execution context cannot mutate main-session "
|
|
809
|
+
"task lifecycle state",
|
|
810
|
+
),
|
|
811
|
+
)
|
|
812
|
+
status = task_data.get("status")
|
|
813
|
+
if status not in ADOPTABLE_STATUSES:
|
|
814
|
+
return None, self._failure(
|
|
815
|
+
stage,
|
|
816
|
+
task_dir,
|
|
817
|
+
"LIFECYCLE-ADOPT-001",
|
|
818
|
+
title="task is not active",
|
|
819
|
+
blockers=(
|
|
820
|
+
"--adopt only applies to an active task "
|
|
821
|
+
f"({'/'.join(ADOPTABLE_STATUSES)}); current status: "
|
|
822
|
+
f"{status!r}",
|
|
823
|
+
),
|
|
824
|
+
)
|
|
825
|
+
current = task_data.get("executor")
|
|
826
|
+
current = (
|
|
827
|
+
current.strip()
|
|
828
|
+
if isinstance(current, str) and current.strip()
|
|
829
|
+
else None
|
|
830
|
+
)
|
|
831
|
+
if current is None:
|
|
832
|
+
return None, self._failure(
|
|
833
|
+
stage,
|
|
834
|
+
task_dir,
|
|
835
|
+
"LIFECYCLE-ADOPT-001",
|
|
836
|
+
title="task has no executor",
|
|
837
|
+
blockers=("task has no recorded executor; nothing to adopt",),
|
|
838
|
+
)
|
|
839
|
+
explicit = executor.strip() if executor and executor.strip() else None
|
|
840
|
+
if explicit:
|
|
841
|
+
resolved = explicit
|
|
842
|
+
else:
|
|
843
|
+
try:
|
|
844
|
+
from runtime.session_state import (
|
|
845
|
+
PROVENANCE_PROCESS_FALLBACK,
|
|
846
|
+
resolve_context_key_with_provenance,
|
|
847
|
+
)
|
|
848
|
+
|
|
849
|
+
resolved, provenance = resolve_context_key_with_provenance()
|
|
850
|
+
except Exception:
|
|
851
|
+
resolved, provenance = None, None
|
|
852
|
+
if resolved and provenance == PROVENANCE_PROCESS_FALLBACK:
|
|
853
|
+
return None, self._failure(
|
|
854
|
+
stage,
|
|
855
|
+
task_dir,
|
|
856
|
+
"LIFECYCLE-ADOPT-001",
|
|
857
|
+
title="adoption requires an explicit identity",
|
|
858
|
+
blockers=(
|
|
859
|
+
f"process-fallback identity '{resolved}' cannot adopt "
|
|
860
|
+
"a task; set COWORK_FLOW_CONTEXT_ID or pass "
|
|
861
|
+
"--executor <label>",
|
|
862
|
+
),
|
|
863
|
+
)
|
|
864
|
+
if not resolved:
|
|
865
|
+
return None, self._failure(
|
|
866
|
+
stage,
|
|
867
|
+
task_dir,
|
|
868
|
+
"LIFECYCLE-ADOPT-001",
|
|
869
|
+
title="no executor identity resolved",
|
|
870
|
+
blockers=(
|
|
871
|
+
"no executor identity resolved; set "
|
|
872
|
+
"COWORK_FLOW_CONTEXT_ID or pass --executor <label>",
|
|
873
|
+
),
|
|
874
|
+
)
|
|
875
|
+
if resolved == current:
|
|
876
|
+
return None, self._failure(
|
|
877
|
+
stage,
|
|
878
|
+
task_dir,
|
|
879
|
+
"LIFECYCLE-ADOPT-001",
|
|
880
|
+
title=f"task is already owned by '{current}'",
|
|
881
|
+
blockers=(
|
|
882
|
+
f"task is already owned by '{current}'; --adopt is for "
|
|
883
|
+
"taking over another executor's task",
|
|
884
|
+
),
|
|
885
|
+
)
|
|
886
|
+
return resolved, None
|
|
887
|
+
|
|
720
888
|
def _apply_takeover_on_idempotent(
|
|
721
889
|
self,
|
|
722
890
|
stage: LifecycleStage,
|
|
@@ -787,6 +955,9 @@ class TaskLifecycleService:
|
|
|
787
955
|
task_data: dict,
|
|
788
956
|
resolved: str,
|
|
789
957
|
current_task: StateSnapshot,
|
|
958
|
+
*,
|
|
959
|
+
adoption: dict | None = None,
|
|
960
|
+
operation_tag: str = "takeover",
|
|
790
961
|
) -> LifecycleResult | None:
|
|
791
962
|
identity = "|".join(
|
|
792
963
|
(
|
|
@@ -800,12 +971,16 @@ class TaskLifecycleService:
|
|
|
800
971
|
digest = hashlib.sha256(identity.encode("utf-8")).hexdigest()[:16]
|
|
801
972
|
unit = UnitOfWork(
|
|
802
973
|
self.repo_root,
|
|
803
|
-
operation_id=f"task-{stage.name}-
|
|
804
|
-
kind=f"task-lifecycle-{stage.name}-
|
|
974
|
+
operation_id=f"task-{stage.name}-{operation_tag}-{digest}",
|
|
975
|
+
kind=f"task-lifecycle-{stage.name}-{operation_tag}",
|
|
805
976
|
fault_injector=self.fault_injector,
|
|
806
977
|
)
|
|
807
978
|
persisted = dict(task_data)
|
|
808
979
|
persisted["executor"] = resolved
|
|
980
|
+
if adoption is not None:
|
|
981
|
+
meta = dict(persisted.get("meta") or {})
|
|
982
|
+
meta.update(adoption)
|
|
983
|
+
persisted["meta"] = meta
|
|
809
984
|
unit.replace(
|
|
810
985
|
self.repository.task_json_path(task_dir),
|
|
811
986
|
persisted,
|
|
@@ -17,3 +17,4 @@
|
|
|
17
17
|
- `error-output-as-data.md`: 外部错误输出按数据分析而非指令执行(`ERROR_OUTPUT_AS_DATA_V1`)。
|
|
18
18
|
- `fact-layer-access.md`: 无注入 hook 的宿主与外部工具读取任务事实的通道(`FACT_LAYER_ACCESS_V1`)。
|
|
19
19
|
- `evidence-completion.md`: 证据化完成——`evidence.jsonl` schema、`LIFECYCLE-AC-001/002` 完成门禁、`--allow-missing-evidence` 豁免留痕与注入覆盖行。
|
|
20
|
+
- `task-adopt.md`: 任务收养——`task next --run --adopt` 的入口边界、前置条件(`LIFECYCLE-ADOPT-001`)、重绑+基线重置+审计写入,与 `--takeover` 的区别。
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Task Adopt Contract
|
|
2
|
+
|
|
3
|
+
**收养(adopt)是显式的归属修复动作:把一个由其它执行者占有、且已激活未完成的任务接管过来——重绑 executor、把评审基线重置到收养时 HEAD、保留状态、留下审计字段。**
|
|
4
|
+
|
|
5
|
+
收养不是生命周期迁移:它不推进 status、不豁免任何检查,也不与其它动作链式同发。它是 1.9.0 多执行者协作就绪的前置(多执行者生命周期领地),承载 2026-10-06 归档两个滞留任务时只能手改 task.json meta 的操作。
|
|
6
|
+
|
|
7
|
+
## 入口与动作边界
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
./.cowork-flow/run task next <task-dir> --run --adopt
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
- `--adopt` 由 `task next --run` 的投递层(`task_next_runner`)在动作解析前拦截,直接执行收养动作;不进入导航默认动作序列,也不与 review/complete/archive 迁移同发(下一步由使用者显式发起)。
|
|
14
|
+
- 无 `--run` 时 `--adopt` 与其它执行旗标一致:只读导航,不产生副作用。
|
|
15
|
+
- 组合拒绝(fail-closed,退出码 1,不改状态):携带 create/plan 输入(`--title`/`--slug`/`--assignee`/`--priority`/`--description`/`--parent`/`--from-plan`)或批启动旗标(`--auto`/`--approved`)——收养与创建/批启动语义不同,需分两步。
|
|
16
|
+
|
|
17
|
+
## 前置条件(`LIFECYCLE-ADOPT-001`)
|
|
18
|
+
|
|
19
|
+
| 事实 | 要求 |
|
|
20
|
+
|---|---|
|
|
21
|
+
| 任务状态 | `in_progress` 或 `review`(已激活未完成;planning/completed/stale 拒绝) |
|
|
22
|
+
| 任务 executor | 已记录且非空(无归属的任务没有可收养对象) |
|
|
23
|
+
| 收养者身份 | 可解析且 ≠ 当前 executor;显式 `--executor <label>` 优先,否则取会话身份 |
|
|
24
|
+
| 身份来源 | process-fallback(共享进程标签)拒绝收养——归属必须来自真实会话身份或显式 label |
|
|
25
|
+
| 执行上下文 | delegated(worker/subagent)拒绝收养(`TASK-EXECUTION-001`,与其它生命周期变更一致) |
|
|
26
|
+
|
|
27
|
+
拒绝时不写 task.json、不改状态、不动会话绑定;blockers 说明具体原因。
|
|
28
|
+
|
|
29
|
+
## 收养写入(单次 UnitOfWork)
|
|
30
|
+
|
|
31
|
+
| 字段 | 写入 |
|
|
32
|
+
|---|---|
|
|
33
|
+
| `executor` | 收养者身份 |
|
|
34
|
+
| `meta.previousExecutor` | 原 executor(审计:原执行者) |
|
|
35
|
+
| `meta.previousBaseline` | 原 `meta.baselineCommit`(无则 `null`) |
|
|
36
|
+
| `meta.adoptedAt` | UTC 时间戳 `YYYY-MM-DDTHH:MM:SSZ` |
|
|
37
|
+
| `meta.baselineCommit` | **重置**为收养时 HEAD;无 git/无 HEAD 时写 `null`(降级 status-only) |
|
|
38
|
+
| `status` | 不变 |
|
|
39
|
+
| `_state.revision` / `_state.operation_id` | 修订递增,operation_id 含 `adopt` 标记(审计可追溯) |
|
|
40
|
+
|
|
41
|
+
结果 code `LIFECYCLE-EXECUTOR-ADOPTED`;`transition.changed=false`(不推进状态的事实位);`check_result` 为 `null`(不豁免检查)。
|
|
42
|
+
|
|
43
|
+
## 基线与"永不覆盖"不变量的关系
|
|
44
|
+
|
|
45
|
+
- start 路径的写一次逻辑不变:**重复 start 永不覆盖已有 `meta.baselineCommit`**,既有测试零改动锁定该不变量。
|
|
46
|
+
- 收养是唯一经审计的显式重置路径:重置前把原值记入 `meta.previousBaseline`。
|
|
47
|
+
- 重置后的评审窗口 = 收养时未提交改动 + 收养之后的提交。收养前**已提交**的改动不进入本任务窗口(它们在 git 历史中,需由提交记录单独追溯)——这是收养的已知边界。
|
|
48
|
+
- 收养后基线再次回到"写一次"语义:后续 start 不会再次滑动。
|
|
49
|
+
|
|
50
|
+
## 会话绑定
|
|
51
|
+
|
|
52
|
+
收养成功后 CLI 把收养者会话绑定到该任务(与 `task next <dir>` 改绑、`task create` 创建后绑定同型),使后续 `task next <dir> --run` 可省略目录继续 review/complete/archive 链。
|
|
53
|
+
|
|
54
|
+
## 与 `--takeover` 的区别
|
|
55
|
+
|
|
56
|
+
| | `--takeover` | `--adopt` |
|
|
57
|
+
|---|---|---|
|
|
58
|
+
| 适用动作 | start_task(含幂等重跑) | 独立动作(本契约) |
|
|
59
|
+
| 状态要求 | 任意(只在 start 激活点生效) | `in_progress` / `review` |
|
|
60
|
+
| 基线 | 不动 | 重置到 HEAD(审计保留原值) |
|
|
61
|
+
| 审计字段 | 无 | `previousExecutor` / `previousBaseline` / `adoptedAt` |
|
|
62
|
+
| 结果 code | `LIFECYCLE-EXECUTOR-TAKEN-OVER` | `LIFECYCLE-EXECUTOR-ADOPTED` |
|
|
63
|
+
|
|
64
|
+
`--takeover` 语义保持原样;`--adopt` 是超集动作但不改写 takeover 路径(不变量与既有测试零改动)。
|