@pennixrv/trellis 0.7.1-beta.9 → 0.7.2-beta.0

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.
Files changed (231) hide show
  1. package/dist/cli/index.js +4 -4
  2. package/dist/cli/index.js.map +1 -1
  3. package/dist/commands/init.d.ts.map +1 -1
  4. package/dist/commands/init.js +39 -26
  5. package/dist/commands/init.js.map +1 -1
  6. package/dist/commands/teamwork/adapters/acp.d.ts +20 -0
  7. package/dist/commands/teamwork/adapters/acp.d.ts.map +1 -0
  8. package/dist/commands/teamwork/adapters/acp.js +268 -0
  9. package/dist/commands/teamwork/adapters/acp.js.map +1 -0
  10. package/dist/commands/teamwork/adapters/claude.d.ts +1 -0
  11. package/dist/commands/teamwork/adapters/claude.d.ts.map +1 -1
  12. package/dist/commands/teamwork/adapters/claude.js +5 -0
  13. package/dist/commands/teamwork/adapters/claude.js.map +1 -1
  14. package/dist/commands/teamwork/adapters/codex.d.ts +3 -2
  15. package/dist/commands/teamwork/adapters/codex.d.ts.map +1 -1
  16. package/dist/commands/teamwork/adapters/codex.js +14 -15
  17. package/dist/commands/teamwork/adapters/codex.js.map +1 -1
  18. package/dist/commands/teamwork/adapters/index.d.ts +23 -1
  19. package/dist/commands/teamwork/adapters/index.d.ts.map +1 -1
  20. package/dist/commands/teamwork/adapters/index.js +163 -24
  21. package/dist/commands/teamwork/adapters/index.js.map +1 -1
  22. package/dist/commands/teamwork/agent-loader.d.ts +2 -1
  23. package/dist/commands/teamwork/agent-loader.d.ts.map +1 -1
  24. package/dist/commands/teamwork/agent-loader.js +2 -1
  25. package/dist/commands/teamwork/agent-loader.js.map +1 -1
  26. package/dist/commands/teamwork/artifact.d.ts +33 -10
  27. package/dist/commands/teamwork/artifact.d.ts.map +1 -1
  28. package/dist/commands/teamwork/artifact.js +182 -50
  29. package/dist/commands/teamwork/artifact.js.map +1 -1
  30. package/dist/commands/teamwork/batch-reservation.d.ts +16 -0
  31. package/dist/commands/teamwork/batch-reservation.d.ts.map +1 -0
  32. package/dist/commands/teamwork/batch-reservation.js +43 -0
  33. package/dist/commands/teamwork/batch-reservation.js.map +1 -0
  34. package/dist/commands/teamwork/batch.d.ts +22 -0
  35. package/dist/commands/teamwork/batch.d.ts.map +1 -0
  36. package/dist/commands/teamwork/batch.js +394 -0
  37. package/dist/commands/teamwork/batch.js.map +1 -0
  38. package/dist/commands/teamwork/context-loader.d.ts +1 -1
  39. package/dist/commands/teamwork/context-loader.d.ts.map +1 -1
  40. package/dist/commands/teamwork/context-loader.js +19 -6
  41. package/dist/commands/teamwork/context-loader.js.map +1 -1
  42. package/dist/commands/teamwork/coordination.d.ts +88 -0
  43. package/dist/commands/teamwork/coordination.d.ts.map +1 -0
  44. package/dist/commands/teamwork/coordination.js +153 -0
  45. package/dist/commands/teamwork/coordination.js.map +1 -0
  46. package/dist/commands/teamwork/coordinator.d.ts +17 -3
  47. package/dist/commands/teamwork/coordinator.d.ts.map +1 -1
  48. package/dist/commands/teamwork/coordinator.js +772 -289
  49. package/dist/commands/teamwork/coordinator.js.map +1 -1
  50. package/dist/commands/teamwork/create.d.ts +1 -0
  51. package/dist/commands/teamwork/create.d.ts.map +1 -1
  52. package/dist/commands/teamwork/create.js +2 -1
  53. package/dist/commands/teamwork/create.js.map +1 -1
  54. package/dist/commands/teamwork/dev-parse-trace.d.ts +2 -1
  55. package/dist/commands/teamwork/dev-parse-trace.d.ts.map +1 -1
  56. package/dist/commands/teamwork/dev-parse-trace.js +2 -0
  57. package/dist/commands/teamwork/dev-parse-trace.js.map +1 -1
  58. package/dist/commands/teamwork/foreground-wait.d.ts +6 -0
  59. package/dist/commands/teamwork/foreground-wait.d.ts.map +1 -0
  60. package/dist/commands/teamwork/foreground-wait.js +65 -0
  61. package/dist/commands/teamwork/foreground-wait.js.map +1 -0
  62. package/dist/commands/teamwork/guard.d.ts +2 -2
  63. package/dist/commands/teamwork/guard.d.ts.map +1 -1
  64. package/dist/commands/teamwork/guard.js +23 -5
  65. package/dist/commands/teamwork/guard.js.map +1 -1
  66. package/dist/commands/teamwork/index.d.ts.map +1 -1
  67. package/dist/commands/teamwork/index.js +139 -10
  68. package/dist/commands/teamwork/index.js.map +1 -1
  69. package/dist/commands/teamwork/kill.d.ts +2 -0
  70. package/dist/commands/teamwork/kill.d.ts.map +1 -1
  71. package/dist/commands/teamwork/kill.js +79 -38
  72. package/dist/commands/teamwork/kill.js.map +1 -1
  73. package/dist/commands/teamwork/messages.d.ts.map +1 -1
  74. package/dist/commands/teamwork/messages.js +3 -0
  75. package/dist/commands/teamwork/messages.js.map +1 -1
  76. package/dist/commands/teamwork/plan.d.ts +22 -0
  77. package/dist/commands/teamwork/plan.d.ts.map +1 -0
  78. package/dist/commands/teamwork/plan.js +288 -0
  79. package/dist/commands/teamwork/plan.js.map +1 -0
  80. package/dist/commands/teamwork/process-identity.d.ts +3 -0
  81. package/dist/commands/teamwork/process-identity.d.ts.map +1 -0
  82. package/dist/commands/teamwork/process-identity.js +24 -0
  83. package/dist/commands/teamwork/process-identity.js.map +1 -0
  84. package/dist/commands/teamwork/profiles.d.ts +43 -6
  85. package/dist/commands/teamwork/profiles.d.ts.map +1 -1
  86. package/dist/commands/teamwork/profiles.js +138 -110
  87. package/dist/commands/teamwork/profiles.js.map +1 -1
  88. package/dist/commands/teamwork/prompt.d.ts +4 -0
  89. package/dist/commands/teamwork/prompt.d.ts.map +1 -0
  90. package/dist/commands/teamwork/prompt.js +33 -0
  91. package/dist/commands/teamwork/prompt.js.map +1 -0
  92. package/dist/commands/teamwork/result.d.ts +54 -0
  93. package/dist/commands/teamwork/result.d.ts.map +1 -0
  94. package/dist/commands/teamwork/result.js +94 -0
  95. package/dist/commands/teamwork/result.js.map +1 -0
  96. package/dist/commands/teamwork/rm.d.ts.map +1 -1
  97. package/dist/commands/teamwork/rm.js +64 -64
  98. package/dist/commands/teamwork/rm.js.map +1 -1
  99. package/dist/commands/teamwork/run.d.ts +5 -0
  100. package/dist/commands/teamwork/run.d.ts.map +1 -1
  101. package/dist/commands/teamwork/run.js +120 -24
  102. package/dist/commands/teamwork/run.js.map +1 -1
  103. package/dist/commands/teamwork/runtime.d.ts +9 -0
  104. package/dist/commands/teamwork/runtime.d.ts.map +1 -0
  105. package/dist/commands/teamwork/runtime.js +372 -0
  106. package/dist/commands/teamwork/runtime.js.map +1 -0
  107. package/dist/commands/teamwork/send.d.ts +1 -0
  108. package/dist/commands/teamwork/send.d.ts.map +1 -1
  109. package/dist/commands/teamwork/send.js +2 -1
  110. package/dist/commands/teamwork/send.js.map +1 -1
  111. package/dist/commands/teamwork/spawn.d.ts +10 -3
  112. package/dist/commands/teamwork/spawn.d.ts.map +1 -1
  113. package/dist/commands/teamwork/spawn.js +41 -29
  114. package/dist/commands/teamwork/spawn.js.map +1 -1
  115. package/dist/commands/teamwork/store/lock.js +28 -4
  116. package/dist/commands/teamwork/store/lock.js.map +1 -1
  117. package/dist/commands/teamwork/supervisor/inbox.d.ts +1 -0
  118. package/dist/commands/teamwork/supervisor/inbox.d.ts.map +1 -1
  119. package/dist/commands/teamwork/supervisor/inbox.js +28 -9
  120. package/dist/commands/teamwork/supervisor/inbox.js.map +1 -1
  121. package/dist/commands/teamwork/supervisor/stdout.d.ts.map +1 -1
  122. package/dist/commands/teamwork/supervisor/stdout.js +2 -3
  123. package/dist/commands/teamwork/supervisor/stdout.js.map +1 -1
  124. package/dist/commands/teamwork/supervisor/warning.d.ts.map +1 -1
  125. package/dist/commands/teamwork/supervisor/warning.js +2 -3
  126. package/dist/commands/teamwork/supervisor/warning.js.map +1 -1
  127. package/dist/commands/teamwork/supervisor.d.ts +5 -0
  128. package/dist/commands/teamwork/supervisor.d.ts.map +1 -1
  129. package/dist/commands/teamwork/supervisor.js +21 -12
  130. package/dist/commands/teamwork/supervisor.js.map +1 -1
  131. package/dist/commands/teamwork/teammate-adapters.d.ts +0 -3
  132. package/dist/commands/teamwork/teammate-adapters.d.ts.map +1 -1
  133. package/dist/commands/teamwork/teammate-adapters.js +9 -54
  134. package/dist/commands/teamwork/teammate-adapters.js.map +1 -1
  135. package/dist/commands/teamwork/teammate-config.d.ts +5 -1
  136. package/dist/commands/teamwork/teammate-config.d.ts.map +1 -1
  137. package/dist/commands/teamwork/teammate-config.js +62 -19
  138. package/dist/commands/teamwork/teammate-config.js.map +1 -1
  139. package/dist/commands/teamwork/teammate-launch.d.ts +14 -4
  140. package/dist/commands/teamwork/teammate-launch.d.ts.map +1 -1
  141. package/dist/commands/teamwork/teammate-launch.js +129 -20
  142. package/dist/commands/teamwork/teammate-launch.js.map +1 -1
  143. package/dist/commands/teamwork/wait.d.ts +2 -0
  144. package/dist/commands/teamwork/wait.d.ts.map +1 -1
  145. package/dist/commands/teamwork/wait.js +44 -3
  146. package/dist/commands/teamwork/wait.js.map +1 -1
  147. package/dist/commands/update.d.ts.map +1 -1
  148. package/dist/commands/update.js +4 -3
  149. package/dist/commands/update.js.map +1 -1
  150. package/dist/configurators/workflow.d.ts.map +1 -1
  151. package/dist/configurators/workflow.js +14 -32
  152. package/dist/configurators/workflow.js.map +1 -1
  153. package/dist/migrations/manifests/0.7.1-beta.10.json +9 -0
  154. package/dist/migrations/manifests/0.7.1-beta.11.json +9 -0
  155. package/dist/migrations/manifests/0.7.1-beta.12.json +9 -0
  156. package/dist/migrations/manifests/0.7.1-beta.13.json +9 -0
  157. package/dist/migrations/manifests/0.7.1-beta.14.json +11 -0
  158. package/dist/migrations/manifests/0.7.1-beta.15.json +9 -0
  159. package/dist/migrations/manifests/0.7.1-beta.16.json +9 -0
  160. package/dist/migrations/manifests/0.7.1-beta.17.json +9 -0
  161. package/dist/migrations/manifests/0.7.1-beta.18.json +9 -0
  162. package/dist/migrations/manifests/0.7.1-beta.19.json +9 -0
  163. package/dist/migrations/manifests/0.7.1-beta.6.json +1 -1
  164. package/dist/migrations/manifests/0.7.2-beta.0.json +11 -0
  165. package/dist/templates/codex/hooks/session-start.py +14 -11
  166. package/dist/templates/codex/hooks.json +10 -0
  167. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/multi-agent-teamwork.md +14 -2
  168. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/task-system.md +1 -1
  169. package/dist/templates/common/bundled-skills/trellis-solution-design-and-convergence/workflow.md +4 -0
  170. package/dist/templates/common/bundled-skills/trellis-task-implementation/workflow.md +6 -0
  171. package/dist/templates/common/bundled-skills/trellis-teamwork/SKILL.md +14 -4
  172. package/dist/templates/common/bundled-skills/trellis-teamwork/references/coordination-modes.md +31 -2
  173. package/dist/templates/common/bundled-skills/trellis-teamwork/references/teammate-configuration-matrix.md +37 -31
  174. package/dist/templates/common/bundled-skills/trellis-teamwork/references/teammate-processes.md +94 -24
  175. package/dist/templates/common/bundled-skills/trellis-teamwork/references/teamwork-plan-construction.md +112 -20
  176. package/dist/templates/copilot/hooks/session-start.py +14 -11
  177. package/dist/templates/copilot/index.d.ts +1 -6
  178. package/dist/templates/copilot/index.d.ts.map +1 -1
  179. package/dist/templates/copilot/index.js +1 -14
  180. package/dist/templates/copilot/index.js.map +1 -1
  181. package/dist/templates/markdown/spec/backend/index.md.txt +4 -35
  182. package/dist/templates/markdown/spec/frontend/index.md.txt +4 -36
  183. package/dist/templates/omp/extensions/trellis/index.ts.txt +1 -1
  184. package/dist/templates/pi/extensions/trellis/index.ts.txt +17 -1
  185. package/dist/templates/shared-hooks/guard-teamwork-wait.py +53 -0
  186. package/dist/templates/shared-hooks/index.d.ts +1 -1
  187. package/dist/templates/shared-hooks/index.d.ts.map +1 -1
  188. package/dist/templates/shared-hooks/index.js +1 -0
  189. package/dist/templates/shared-hooks/index.js.map +1 -1
  190. package/dist/templates/shared-hooks/inject-workflow-state.py +8 -3
  191. package/dist/templates/shared-hooks/session-start.py +16 -12
  192. package/dist/templates/trellis/agents/delegated-worktree-primary-agent.md +9 -0
  193. package/dist/templates/trellis/agents/profiles/architecture.md +9 -0
  194. package/dist/templates/trellis/agents/profiles/diagnosis.md +9 -0
  195. package/dist/templates/trellis/agents/profiles/exploration.md +9 -0
  196. package/dist/templates/trellis/agents/profiles/research.md +9 -0
  197. package/dist/templates/trellis/agents/profiles/review.md +9 -0
  198. package/dist/templates/trellis/agents/profiles/verification.md +9 -0
  199. package/dist/templates/trellis/agents/teammate-profiles.json +56 -4
  200. package/dist/templates/trellis/agents/teammate.md +4 -63
  201. package/dist/templates/trellis/config.yaml +2 -5
  202. package/dist/templates/trellis/index.d.ts +1 -0
  203. package/dist/templates/trellis/index.d.ts.map +1 -1
  204. package/dist/templates/trellis/index.js +16 -4
  205. package/dist/templates/trellis/index.js.map +1 -1
  206. package/dist/templates/trellis/scripts/common/task_continuity.py +54 -28
  207. package/dist/templates/trellis/scripts/common/task_store.py +7 -2
  208. package/dist/templates/trellis/scripts/common/task_workflow.py +270 -18
  209. package/dist/templates/trellis/scripts/task.py +27 -3
  210. package/dist/templates/trellis/teamwork.json +10 -0
  211. package/dist/templates/trellis/workflow.md +7 -0
  212. package/dist/utils/asset-backup.d.ts.map +1 -1
  213. package/dist/utils/asset-backup.js +10 -4
  214. package/dist/utils/asset-backup.js.map +1 -1
  215. package/dist/utils/template-fetcher.d.ts +4 -19
  216. package/dist/utils/template-fetcher.d.ts.map +1 -1
  217. package/dist/utils/template-fetcher.js +9 -45
  218. package/dist/utils/template-fetcher.js.map +1 -1
  219. package/package.json +3 -2
  220. package/dist/templates/copilot/prompts/before-dev.prompt.md +0 -39
  221. package/dist/templates/copilot/prompts/brainstorm.prompt.md +0 -13
  222. package/dist/templates/copilot/prompts/break-loop.prompt.md +0 -129
  223. package/dist/templates/copilot/prompts/check-cross-layer.prompt.md +0 -170
  224. package/dist/templates/copilot/prompts/check.prompt.md +0 -110
  225. package/dist/templates/copilot/prompts/create-command.prompt.md +0 -116
  226. package/dist/templates/copilot/prompts/finish-work.prompt.md +0 -22
  227. package/dist/templates/copilot/prompts/integrate-skill.prompt.md +0 -223
  228. package/dist/templates/copilot/prompts/onboard.prompt.md +0 -362
  229. package/dist/templates/copilot/prompts/record-session.prompt.md +0 -58
  230. package/dist/templates/copilot/prompts/start.prompt.md +0 -65
  231. package/dist/templates/copilot/prompts/update-spec.prompt.md +0 -358
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import argparse
6
6
  import contextlib
7
+ import copy
7
8
  from datetime import datetime, timezone
8
9
  import hashlib
9
10
  import json
@@ -133,6 +134,10 @@ def implementation_start_ready(data: dict, task_dir: Path) -> bool:
133
134
  phase_ids = state.get("phase_ids")
134
135
  if not isinstance(phase_ids, list) or PHASE_IDS[3] not in phase_ids:
135
136
  return False
137
+ phases = state.get("phases")
138
+ implementation = phases.get(PHASE_IDS[3]) if isinstance(phases, dict) else None
139
+ if not isinstance(implementation, dict) or implementation.get("status") != "active":
140
+ return False
136
141
  if state.get("task_kind") not in {"implementation", "combined"}:
137
142
  return False
138
143
  authorization = state.get("implementation_authorization")
@@ -228,20 +233,23 @@ def _phase(state: dict, phase_id: str | None = None) -> tuple[str, dict]:
228
233
  return current, record
229
234
 
230
235
 
231
- def _require_current_requirements_confirmation(task_dir: Path, state: dict) -> None:
232
- if PHASE_IDS[0] not in state["phase_ids"]:
233
- return
236
+ def _is_withdrawal_route(state: dict) -> bool:
234
237
  requirements = state["phases"].get(PHASE_IDS[0], {})
235
238
  withdrawal = state.get("return_record")
236
239
  skip_records = state.get("skip_records")
237
240
  skipped_requirements = skip_records.get(PHASE_IDS[0]) if isinstance(skip_records, dict) else None
238
- if (
241
+ return (
239
242
  state["phase_ids"] == [PHASE_IDS[0], PHASE_IDS[4]]
240
- and state.get("current_phase_id") == PHASE_IDS[4]
243
+ and state.get("current_phase_id") in {PHASE_IDS[4], None}
241
244
  and isinstance(requirements, dict)
242
- and requirements.get("status") == "skipped"
243
- and isinstance(skipped_requirements, dict)
244
- and skipped_requirements.get("task_route_revision") == state.get("task_route_revision")
245
+ and (
246
+ requirements.get("status") == "accepted"
247
+ or (
248
+ requirements.get("status") == "skipped"
249
+ and isinstance(skipped_requirements, dict)
250
+ and skipped_requirements.get("task_route_revision") == state.get("task_route_revision")
251
+ )
252
+ )
245
253
  and isinstance(withdrawal, dict)
246
254
  and isinstance(withdrawal.get("from"), str)
247
255
  and withdrawal["from"] in PHASE_ID_SET
@@ -249,7 +257,14 @@ def _require_current_requirements_confirmation(task_dir: Path, state: dict) -> N
249
257
  and withdrawal.get("task_route_revision") == state.get("task_route_revision")
250
258
  and isinstance(withdrawal.get("evidence_refs"), list)
251
259
  and bool(withdrawal["evidence_refs"])
252
- ):
260
+ )
261
+
262
+
263
+ def _require_current_requirements_confirmation(task_dir: Path, state: dict) -> None:
264
+ if PHASE_IDS[0] not in state["phase_ids"]:
265
+ return
266
+ requirements = state["phases"].get(PHASE_IDS[0], {})
267
+ if requirements.get("status") == "skipped" and _is_withdrawal_route(state):
253
268
  return
254
269
  acceptance = requirements.get("acceptance") if isinstance(requirements, dict) else None
255
270
  if (
@@ -441,6 +456,31 @@ def _show(state: dict) -> int:
441
456
  return 0
442
457
 
443
458
 
459
+ def _require_acceptance_coverage(task_dir: Path, contract_ref: str) -> None:
460
+ _artifact_digest(task_dir, [contract_ref])
461
+ lines = (task_dir / contract_ref).read_text(encoding="utf-8").splitlines()
462
+ heading = next((index for index, line in enumerate(lines) if line.strip() == "## 验收覆盖"), None)
463
+ headers = ["接受条件", "工作包", "Owner", "验证入口", "交付证据", "停止条件"]
464
+ if heading is not None:
465
+ section = []
466
+ for line in lines[heading + 1:]:
467
+ if line.startswith("#"):
468
+ break
469
+ if line.strip():
470
+ section.append(line.strip())
471
+ rows = [line.strip("|").split("|") for line in section if line.startswith("|") and line.endswith("|")]
472
+ rows = [[cell.strip() for cell in row] for row in rows]
473
+ if (
474
+ len(rows) >= 3
475
+ and rows[0] == headers
476
+ and len(rows[1]) == len(headers)
477
+ and all(re.fullmatch(r":?-{3,}:?", cell) for cell in rows[1])
478
+ and all(len(row) == len(headers) and all(row) for row in rows[2:])
479
+ ):
480
+ return
481
+ raise TaskWorkflowError("TASK_PHASE_COVERAGE_REQUIRED", "implementation contract requires ## 验收覆盖 with nonempty rows: " + " | ".join(headers))
482
+
483
+
444
484
  def _update(args: argparse.Namespace, task_dir: Path, data: dict, state: dict) -> int:
445
485
  action = args.phase_action
446
486
  child_dispositions: dict[str, str] = {}
@@ -460,11 +500,12 @@ def _update(args: argparse.Namespace, task_dir: Path, data: dict, state: dict) -
460
500
  current, record = PHASE_IDS[4], state["phases"][PHASE_IDS[4]]
461
501
  else:
462
502
  current, record = _phase(state)
503
+ accepted_design_return = action == "return" and current == PHASE_IDS[2] and record.get("status") == "accepted"
463
504
  returning_to_requirements = action == "return" and args.target_phase == PHASE_IDS[0]
464
505
  rerouting_to_requirements = action == "route" and args.target_phase == PHASE_IDS[0]
465
506
  if current != PHASE_IDS[0] and not returning_to_requirements and not rerouting_to_requirements:
466
507
  _require_current_requirements_confirmation(task_dir, state)
467
- if action not in {"show", "resume", "route", "authorize", "admission"} and record.get("status") != "active" and not reviewed_delivery:
508
+ if action not in {"show", "resume", "route", "authorize", "admission"} and record.get("status") != "active" and not reviewed_delivery and not accepted_design_return:
468
509
  raise TaskWorkflowError("TASK_PHASE_INVALID_TRANSITION", "only the active phase can be changed")
469
510
 
470
511
  if action == "artifact":
@@ -526,6 +567,8 @@ def _update(args: argparse.Namespace, task_dir: Path, data: dict, state: dict) -
526
567
  raise TaskWorkflowError("TASK_PHASE_INVALID_TRANSITION", "Solution Design and Convergence Phase must remain active for pending_user_decision or deferred; accept implementation_contract, analysis_conclusion, or no_go")
527
568
  if result_ref not in record["artifact_refs"]:
528
569
  raise TaskWorkflowError("TASK_PHASE_RESULT_NOT_BOUND", "--result-ref must be included in the current phase artifact refs")
570
+ if current == PHASE_IDS[2] and outcome == "implementation_contract":
571
+ _require_acceptance_coverage(task_dir, result_ref)
529
572
  if current == PHASE_IDS[3]:
530
573
  authorization = state.get("implementation_authorization")
531
574
  if not isinstance(authorization, dict) or authorization.get("task_route_revision") != state["task_route_revision"]:
@@ -536,8 +579,7 @@ def _update(args: argparse.Namespace, task_dir: Path, data: dict, state: dict) -
536
579
  if outcome != args.closeout_disposition:
537
580
  raise TaskWorkflowError("TASK_PHASE_INVALID_INPUT", "Delivery Review and On-Site Closeout Phase outcome must match --closeout-disposition")
538
581
  if args.closeout_disposition == "cancelled":
539
- withdrawal = state.get("return_record")
540
- if not isinstance(withdrawal, dict) or withdrawal.get("to") != PHASE_IDS[4] or not withdrawal.get("evidence_refs"):
582
+ if not _is_withdrawal_route(state):
541
583
  raise TaskWorkflowError("TASK_PHASE_USER_CONFIRMATION_REQUIRED", "cancellation closeout requires a user-confirmed route to Delivery Review and On-Site Closeout Phase")
542
584
  child_dispositions = _validate_child_dispositions(
543
585
  task_dir, data, args.closeout_disposition, args.child_disposition
@@ -563,17 +605,38 @@ def _update(args: argparse.Namespace, task_dir: Path, data: dict, state: dict) -
563
605
  _advance(state, current)
564
606
  elif action == "return":
565
607
  target = _required_text(args.target_phase, "--target-phase")
608
+ return_kind = args.return_kind
609
+ execution_rework = return_kind == "execution_rework"
566
610
  if target not in state["phase_ids"] or (
567
611
  state["phase_ids"].index(target) >= state["phase_ids"].index(current)
568
- and not (reviewed_delivery and target == current)
612
+ and not ((reviewed_delivery or accepted_design_return or execution_rework) and target == current)
569
613
  ):
570
614
  raise TaskWorkflowError("TASK_PHASE_INVALID_TRANSITION", "return target must be an earlier phase in the current route")
571
615
  reason = _required_text(args.reason, "--reason")
572
616
  evidence = _refs(args.evidence_ref, "--evidence-ref", required=True)
617
+ authorization = state.get("implementation_authorization")
618
+ admission = state.get("route_dependency_admission")
619
+ if execution_rework:
620
+ if current not in PHASE_IDS[3:] or target not in PHASE_IDS[3:] or PHASE_IDS[3] not in state["phase_ids"]:
621
+ raise TaskWorkflowError("TASK_PHASE_INVALID_TRANSITION", "execution_rework is limited to implementation and delivery phases")
622
+ candidate = copy.deepcopy(data)
623
+ candidate_state = candidate["meta"]["task_workflow"]
624
+ _invalidate_from(candidate_state, target, keep_first=True)
625
+ candidate_state["implementation_authorization"] = authorization
626
+ _require_current_accepted_artifacts(task_dir, candidate_state)
627
+ candidate_state["current_phase_id"] = PHASE_IDS[3]
628
+ candidate_state["phases"][PHASE_IDS[3]]["status"] = "active"
629
+ if not implementation_start_ready(candidate, task_dir):
630
+ raise TaskWorkflowError("TASK_PHASE_IMPLEMENTATION_NOT_AUTHORIZED", "execution_rework requires the unchanged authorized implementation contract")
631
+ if admission is not None and (admission.get("admission_state") != "ready" or admission.get("task_route_revision") != state["task_route_revision"]):
632
+ raise TaskWorkflowError("TASK_PHASE_ADMISSION_REQUIRED", "execution_rework requires current ready dependency admission")
573
633
  state["input_revision"] = _increment(state["input_revision"])
574
634
  _invalidate_from(state, target, keep_first=True)
635
+ if execution_rework:
636
+ state["implementation_authorization"] = authorization
637
+ state["route_dependency_admission"] = admission
575
638
  state["phases"][target]["input_revision"] = state["input_revision"]
576
- state["return_record"] = {"from": current, "to": target, "reason": reason, "evidence_refs": evidence, "task_route_revision": state["task_route_revision"]}
639
+ state["return_record"] = {"from": current, "to": target, "return_kind": return_kind, "authorization_retained": execution_rework, "reason": reason, "evidence_refs": evidence, "task_route_revision": state["task_route_revision"]}
577
640
  elif action == "route":
578
641
  phases = _validate_route(args.phase_ids)
579
642
  task_kind = args.task_kind if args.task_kind is not None else state["task_kind"]
@@ -685,6 +748,7 @@ def _update(args: argparse.Namespace, task_dir: Path, data: dict, state: dict) -
685
748
  raise TaskWorkflowError("TASK_PHASE_STALE_ARTIFACT", "Solution Design and Convergence Phase implementation contract changed after acceptance; refresh and accept the current artifact before authorization")
686
749
  elif contract_ref not in state.get("skip_records", {}).get(PHASE_IDS[2], {}).get("evidence_refs", []):
687
750
  raise TaskWorkflowError("TASK_PHASE_CONTRACT_NOT_ACCEPTED", "when Solution Design and Convergence Phase is omitted, the implementation contract must be among its route evidence")
751
+ _require_acceptance_coverage(task_dir, contract_ref)
688
752
  state["implementation_authorization"] = {"contract_ref": contract_ref, "contract_revision": _artifact_digest(task_dir, [contract_ref]), "approval_ref": approval_ref, "task_route_revision": state["task_route_revision"]}
689
753
  if current == PHASE_IDS[2]:
690
754
  _advance(state, current)
@@ -745,6 +809,56 @@ def _paseo_list(args: list[str], cwd: Path, code: str) -> list[dict]:
745
809
  return value
746
810
 
747
811
 
812
+ def _delegation_binding(repo_root: Path, child_dir: Path, child_data: dict, result_path: str) -> dict:
813
+ if not isinstance(result_path, str) or Path(result_path).is_absolute() or ".." in Path(result_path).parts or not result_path.startswith(".trellis/delegated-results/"):
814
+ raise TaskWorkflowError("TASK_DELEGATE_CONTRACT_INVALID", "result path must stay under .trellis/delegated-results/")
815
+ try:
816
+ contract_path = (child_dir / "prd.md").resolve(strict=True)
817
+ contract_path.relative_to(child_dir.resolve())
818
+ role_path = (repo_root / ".trellis/agents/delegated-worktree-primary-agent.md").resolve(strict=True)
819
+ role_path.relative_to((repo_root / ".trellis/agents").resolve())
820
+ if not contract_path.is_file() or not role_path.is_file() or contract_path.stat().st_size + role_path.stat().st_size > 1048576:
821
+ raise ValueError("delegated contract and role must be regular files within the size budget")
822
+ contract = contract_path.read_bytes()
823
+ role = role_path.read_bytes()
824
+ except (OSError, ValueError) as error:
825
+ raise TaskWorkflowError("TASK_DELEGATE_CONTRACT_INVALID", str(error)) from error
826
+ if not contract.strip() or not role.strip():
827
+ raise TaskWorkflowError("TASK_DELEGATE_CONTRACT_INVALID", "child PRD and delegated role must be nonempty")
828
+ return {
829
+ "contract_sha256": hashlib.sha256(contract).hexdigest(),
830
+ "role_sha256": hashlib.sha256(role).hexdigest(),
831
+ "branch": _required_text(child_data.get("branch"), "child branch"),
832
+ "base_commit": _git_output(["rev-parse", "--verify", "--end-of-options", f"{_required_text(child_data.get('base_branch'), 'child base_branch')}^{{commit}}"], repo_root),
833
+ "result_path": result_path,
834
+ }
835
+
836
+
837
+ def cmd_delegate_bind(args: argparse.Namespace) -> int:
838
+ try:
839
+ repo_root = get_repo_root().resolve()
840
+ child_dir = _task_dir(args.child_task)
841
+ child, _ = _load(child_dir)
842
+ if not isinstance(child.get("parent"), str) or not child["parent"].strip():
843
+ raise TaskWorkflowError("TASK_DELEGATE_PARENT_LINK_INVALID", "child task has no parent link")
844
+ parent_dir = _task_dir(child["parent"])
845
+ parent, meta = _load(parent_dir)
846
+ if child_dir.name not in parent.get("children", []) or not implementation_start_ready(parent, parent_dir):
847
+ raise TaskWorkflowError("TASK_DELEGATE_IMPLEMENTATION_NOT_AUTHORIZED", "bind only a linked child within current parent implementation authorization")
848
+ state = _state(meta)
849
+ authorization = state["implementation_authorization"]
850
+ result_path = args.result_path or f".trellis/delegated-results/{child_dir.name}.md"
851
+ binding = _delegation_binding(repo_root, child_dir, child, result_path)
852
+ binding.update({"authorization_ref": authorization["approval_ref"], "parent_contract_revision": authorization["contract_revision"], "task_route_revision": state["task_route_revision"]})
853
+ meta.setdefault("delegation_authorizations", {})[child_dir.name] = binding
854
+ _save(parent_dir / FILE_TASK_JSON, parent)
855
+ print(json.dumps(binding, ensure_ascii=False))
856
+ return 0
857
+ except TaskWorkflowError as error:
858
+ print(f"{error.code}: {error}", file=sys.stderr)
859
+ return 1
860
+
861
+
748
862
  def cmd_delegate_start(args: argparse.Namespace) -> int:
749
863
  try:
750
864
  repo_root = get_repo_root().resolve()
@@ -760,6 +874,11 @@ def cmd_delegate_start(args: argparse.Namespace) -> int:
760
874
  child_data, child_meta = _load(child_dir)
761
875
  if child_data.get("parent") != parent_dir.name or child_dir.name not in parent_data.get("children", []):
762
876
  raise TaskWorkflowError("TASK_DELEGATE_PARENT_LINK_INVALID", "parent and child task links do not match")
877
+ from .task_continuity import assert_task_mutation_allowed
878
+
879
+ assert_task_mutation_allowed(repo_root, parent_dir, parent_data)
880
+ if child_data.get("status") not in {"planning", "in_progress"}:
881
+ raise TaskWorkflowError("TASK_DELEGATE_CHILD_TERMINAL", "only a planning or in-progress child task can be delegated")
763
882
  if parent_data.get("status") != "in_progress" or not implementation_start_ready(parent_data, parent_dir):
764
883
  raise TaskWorkflowError("TASK_DELEGATE_IMPLEMENTATION_NOT_AUTHORIZED", "parent task is not at an authorized Task Implementation and Test Verification Phase entry")
765
884
 
@@ -813,6 +932,7 @@ def cmd_delegate_start(args: argparse.Namespace) -> int:
813
932
  raise TaskWorkflowError("TASK_DELEGATE_PASEO_UNAVAILABLE", f"could not run Paseo: {error}") from error
814
933
  if version.returncode != 0:
815
934
  raise TaskWorkflowError("TASK_DELEGATE_PASEO_UNAVAILABLE", (version.stderr or version.stdout).strip() or "Paseo CLI preflight failed")
935
+ _paseo_list(["agent", "ls", "--all", "--global", "--background", "--json"], repo_root, "TASK_DELEGATE_PASEO_UNAVAILABLE")
816
936
  role_path = repo_root / ".trellis" / "agents" / "delegated-worktree-primary-agent.md"
817
937
  try:
818
938
  role = role_path.read_text(encoding="utf-8")
@@ -823,6 +943,16 @@ def cmd_delegate_start(args: argparse.Namespace) -> int:
823
943
  if not isinstance(authorization, dict) or not isinstance(authorization.get("approval_ref"), str):
824
944
  raise TaskWorkflowError("TASK_DELEGATE_AUTHORIZATION_INVALID", "current implementation authorization is invalid")
825
945
  contract_digest = hashlib.sha256(contract_bytes).hexdigest()
946
+ bindings = parent_meta.get("delegation_authorizations", {})
947
+ binding = bindings.get(child_dir.name) if isinstance(bindings, dict) else None
948
+ if not isinstance(binding, dict):
949
+ raise TaskWorkflowError("TASK_DELEGATE_CONTRACT_NOT_BOUND", "freeze this child through delegate-bind before dispatch")
950
+ actual = _delegation_binding(repo_root, child_dir, child_data, binding.get("result_path", ""))
951
+ if any(binding.get(key) != value for key, value in actual.items()) or actual["contract_sha256"] != contract_digest or actual["role_sha256"] != hashlib.sha256(role.encode("utf-8")).hexdigest() or binding.get("authorization_ref") != authorization["approval_ref"] or binding.get("parent_contract_revision") != authorization["contract_revision"] or binding.get("task_route_revision") != state["task_route_revision"]:
952
+ raise TaskWorkflowError("TASK_DELEGATE_STALE_CONTRACT", "child PRD, role, Git base, output path, or parent authorization changed; review and bind the child again")
953
+ prompt_bytes = len((role + contract).encode("utf-8"))
954
+ if prompt_bytes > (24000 if os.name == "nt" else 96000):
955
+ raise TaskWorkflowError("TASK_DELEGATE_PROMPT_TOO_LARGE", "delegated prompt exceeds the native Paseo argv budget; narrow the contract before creating resources")
826
956
  previous = child_meta.get("delegated_worktree_execution_record")
827
957
  reuse_workspace = False
828
958
  workspace_id = None
@@ -846,7 +976,7 @@ def cmd_delegate_start(args: argparse.Namespace) -> int:
846
976
  previous_worktree = Path(previous_worktree_path).expanduser().resolve()
847
977
  agents = _paseo_list([
848
978
  "agent", "ls", "--all", "--global", "--background", "--json",
849
- "--label", f"trellis.task_id={child_dir.name}",
979
+ "--label", f"trellis.task_id={previous.get('label_task_id', child_dir.name)}",
850
980
  ], repo_root, "TASK_DELEGATE_RECONCILIATION_REQUIRED")
851
981
  for agent in agents:
852
982
  agent_id = agent.get("id")
@@ -897,9 +1027,14 @@ def cmd_delegate_start(args: argparse.Namespace) -> int:
897
1027
  "workspace_id": workspace_id,
898
1028
  "worktree_path": str(worktree) if worktree else None,
899
1029
  "agent_id": None,
1030
+ "label_task_id": child_dir.name,
1031
+ "role_sha256": actual["role_sha256"],
1032
+ "result_path": actual["result_path"],
900
1033
  "status": "starting",
901
1034
  "started_at": datetime.now(timezone.utc).isoformat(),
902
1035
  }
1036
+ if previous is not None:
1037
+ child_meta.setdefault("delegated_worktree_execution_history", []).append(previous)
903
1038
  child_meta["delegated_worktree_execution_record"] = record
904
1039
  _save(child_dir / FILE_TASK_JSON, child_data)
905
1040
 
@@ -940,6 +1075,7 @@ def cmd_delegate_start(args: argparse.Namespace) -> int:
940
1075
  "the parent authorization and Git baseline are execution metadata. Do not accept task phases or change task lifecycle state.\n\n"
941
1076
  f"Parent task: {parent_dir.name}\nParent route revision: {state['task_route_revision']}\n"
942
1077
  f"Authorized contract: prd.md\nBranch: {branch}\nBase commit: {base_commit}\n\n"
1078
+ f"Assigned result artifact: {actual['result_path']}\n"
943
1079
  f"{role}\n\n## Child task prd.md\n{contract}"
944
1080
  )
945
1081
  agent = _paseo_json([
@@ -1001,6 +1137,56 @@ def cmd_cancel_task(args: argparse.Namespace) -> int:
1001
1137
  return 1
1002
1138
 
1003
1139
 
1140
+ def cmd_reopen_task(args: argparse.Namespace) -> int:
1141
+ try:
1142
+ task_dir = _task_dir(args.task_dir)
1143
+ with _task_lock(task_dir):
1144
+ data, meta = _load(task_dir)
1145
+ previous_status = data.get("status")
1146
+ if previous_status not in {"completed", "cancelled"}:
1147
+ raise TaskWorkflowError("TASK_PHASE_REOPEN_INVALID_STATUS", "only completed or cancelled tasks can be reopened")
1148
+ reason = _required_text(args.reason, "--reason")
1149
+ evidence = _refs(args.evidence_ref, "--evidence-ref", required=True)
1150
+ _assert_delegated_resources_closed(task_dir, data)
1151
+ recoveries = meta.setdefault("task_lifecycle_recoveries", [])
1152
+ if not isinstance(recoveries, list) or any(not isinstance(record, dict) for record in recoveries):
1153
+ raise TaskWorkflowError("TASK_PHASE_TASK_INVALID", "task lifecycle recovery history is malformed")
1154
+ recovery = {
1155
+ "from_status": previous_status,
1156
+ "to_status": "planning",
1157
+ "reason": reason,
1158
+ "evidence_refs": evidence,
1159
+ "reopened_at": datetime.now(timezone.utc).isoformat(),
1160
+ "terminal_completed_at": data.get("completedAt"),
1161
+ "terminal_cancellation": copy.deepcopy(meta.get("task_cancellation")),
1162
+ }
1163
+ state = meta.get("task_workflow")
1164
+ if state is not None:
1165
+ state = _state(meta)
1166
+ state["implementation_authorization"] = None
1167
+ state["route_dependency_admission"] = None
1168
+ state["closeout_disposition"] = None
1169
+ recoveries.append(recovery)
1170
+ data["status"] = "planning"
1171
+ data["completedAt"] = None
1172
+ from .task_move import relocate_task
1173
+ from .active_task import clear_task_from_sessions
1174
+ from .task_continuity import clear_task_runtime_records
1175
+
1176
+ repo_root = get_repo_root()
1177
+ # Relocation rewrites runtime paths; terminal runtime must be discarded first.
1178
+ for runtime_dir in (task_dir, repo_root / ".trellis/tasks/planning" / task_dir.name):
1179
+ clear_task_from_sessions(str(runtime_dir), repo_root)
1180
+ if not clear_task_runtime_records(repo_root, runtime_dir.relative_to(repo_root).as_posix()):
1181
+ raise TaskWorkflowError("TASK_PHASE_RUNTIME_CLEANUP_FAILED", "could not discard terminal task runtime; task remains terminal")
1182
+ task_dir = relocate_task(repo_root, task_dir, data)
1183
+ print(json.dumps({"status": "planning", "task_lifecycle_recovery": recovery, "runtime_records_cleared": True}, ensure_ascii=False))
1184
+ return 0
1185
+ except TaskWorkflowError as error:
1186
+ print(f"{error.code}: {error}", file=sys.stderr)
1187
+ return 1
1188
+
1189
+
1004
1190
  def task_closeout_readiness(data: dict, disposition: str) -> str | None:
1005
1191
  if "meta" in data and not isinstance(data["meta"], dict):
1006
1192
  return "task metadata is malformed"
@@ -1022,8 +1208,7 @@ def task_closeout_readiness(data: dict, disposition: str) -> str | None:
1022
1208
  if state.get("closeout_disposition") != disposition:
1023
1209
  return f"Delivery Review and On-Site Closeout Phase disposition is {state.get('closeout_disposition')}; expected {disposition}"
1024
1210
  if disposition == "cancelled":
1025
- withdrawal = state.get("return_record")
1026
- if not isinstance(withdrawal, dict) or withdrawal.get("to") != PHASE_IDS[4] or not withdrawal.get("evidence_refs"):
1211
+ if not _is_withdrawal_route(state):
1027
1212
  return "cancellation requires a user-confirmed route to Delivery Review and On-Site Closeout Phase"
1028
1213
  return None
1029
1214
 
@@ -1073,6 +1258,67 @@ def _validate_child_dispositions(
1073
1258
  return choices
1074
1259
 
1075
1260
 
1261
+ def cmd_delegate_reconcile(args: argparse.Namespace) -> int:
1262
+ try:
1263
+ root = get_repo_root().resolve()
1264
+ task_dir = _task_dir(args.child_task)
1265
+ data, meta = _load(task_dir)
1266
+ record = meta.get("delegated_worktree_execution_record")
1267
+ if not isinstance(record, dict):
1268
+ raise TaskWorkflowError("TASK_DELEGATE_RECONCILIATION_REQUIRED", "no valid delegated execution mapping")
1269
+ if record.get("status") == "retired" and isinstance(record.get("closure_evidence"), dict):
1270
+ print(json.dumps(record, ensure_ascii=False))
1271
+ return 0
1272
+ label = record.get("label_task_id", task_dir.name)
1273
+ agents = _paseo_list(["agent", "ls", "--all", "--global", "--background", "--json", "--label", f"trellis.task_id={label}"], root, "TASK_DELEGATE_RECONCILIATION_REQUIRED")
1274
+ if any(agent.get("status") != "closed" for agent in agents):
1275
+ raise TaskWorkflowError("TASK_PHASE_RESOURCE_ACTIVE", "a delegated Paseo agent is still active or unknown")
1276
+ agent_id = record.get("agent_id")
1277
+ if agent_id and not any(agent.get("id") == agent_id for agent in agents):
1278
+ raise TaskWorkflowError("TASK_DELEGATE_RECONCILIATION_REQUIRED", "recorded agent is not available; preserve its mapping until native closure evidence is available")
1279
+ worktree_path = record.get("worktree_path")
1280
+ workspace_id = record.get("workspace_id")
1281
+ workspaces = _paseo_list(["workspace", "ls", "--background", "--json"], root, "TASK_DELEGATE_RECONCILIATION_REQUIRED")
1282
+ workspace = next((row for row in workspaces if row.get("workspaceId") == workspace_id), None) if workspace_id else None
1283
+ if workspace and (not isinstance(worktree_path, str) or Path(workspace.get("cwd", "")).resolve() != Path(worktree_path).resolve()):
1284
+ raise TaskWorkflowError("TASK_DELEGATE_RECONCILIATION_REQUIRED", "workspace mapping does not match")
1285
+ head = None
1286
+ if isinstance(worktree_path, str) and Path(worktree_path).is_dir():
1287
+ worktree = Path(worktree_path).resolve()
1288
+ common = Path(_git_output(["rev-parse", "--git-common-dir"], worktree))
1289
+ common = (common if common.is_absolute() else worktree / common).resolve()
1290
+ expected_common = Path(_git_output(["rev-parse", "--git-common-dir"], root))
1291
+ expected_common = (expected_common if expected_common.is_absolute() else root / expected_common).resolve()
1292
+ if common != expected_common or _git_output(["branch", "--show-current"], worktree) != record.get("branch"):
1293
+ raise TaskWorkflowError("TASK_DELEGATE_RECONCILIATION_REQUIRED", "worktree does not belong to the recorded repository and branch")
1294
+ dirty = _git_output(["status", "--porcelain", "--untracked-files=all"], worktree)
1295
+ allowed = "?? " + record.get("result_path", "")
1296
+ if any(line != allowed for line in dirty.splitlines()):
1297
+ raise TaskWorkflowError("TASK_DELEGATE_RECONCILIATION_REQUIRED", "worktree contains unintegrated changes; preserve and integrate them before retiring")
1298
+ head = _git_output(["rev-parse", "HEAD"], worktree)
1299
+ if run_git(["merge-base", "--is-ancestor", head, "HEAD"], cwd=root)[0] != 0:
1300
+ raise TaskWorkflowError("TASK_DELEGATE_RECONCILIATION_REQUIRED", "worktree commits are not integrated into the coordinator checkout")
1301
+ elif workspace:
1302
+ raise TaskWorkflowError("TASK_DELEGATE_RECONCILIATION_REQUIRED", "workspace still exists but its worktree cannot be inspected")
1303
+ elif not agent_id:
1304
+ branch = record.get("branch")
1305
+ if not isinstance(branch, str) or run_git(["show-ref", "--verify", "--quiet", f"refs/heads/{branch}"], cwd=root)[0] != 1:
1306
+ raise TaskWorkflowError("TASK_DELEGATE_RECONCILIATION_REQUIRED", "incomplete mapping still has an unresolved branch")
1307
+ evidence = {"agent_ids": [agent.get("id") for agent in agents], "workspace_id": workspace_id, "head": head, "checked_at": datetime.now(timezone.utc).isoformat()}
1308
+ if not args.execute:
1309
+ print(json.dumps({"status": "ready", "evidence": evidence}, ensure_ascii=False))
1310
+ return 0
1311
+ reason = _required_text(args.reason, "--reason")
1312
+ approval_ref = _required_text(args.approval_ref, "--approval-ref")
1313
+ record.update({"status": "retired", "closure_evidence": {**evidence, "reason": reason, "approval_ref": approval_ref}})
1314
+ _save(task_dir / FILE_TASK_JSON, data)
1315
+ print(json.dumps(record, ensure_ascii=False))
1316
+ return 0
1317
+ except TaskWorkflowError as error:
1318
+ print(f"{error.code}: {error}", file=sys.stderr)
1319
+ return 1
1320
+
1321
+
1076
1322
  def _assert_delegated_resources_closed(task_dir: Path, data: dict) -> None:
1077
1323
  children = data.get("children", [])
1078
1324
  if not isinstance(children, list):
@@ -1085,17 +1331,23 @@ def _assert_delegated_resources_closed(task_dir: Path, data: dict) -> None:
1085
1331
  def _assert_delegated_record_closed(task_id: str, record) -> None:
1086
1332
  if record is None:
1087
1333
  return
1334
+ if isinstance(record, dict) and record.get("status") == "retired" and isinstance(record.get("closure_evidence"), dict):
1335
+ return
1088
1336
  agent_id = record.get("agent_id") if isinstance(record, dict) else None
1089
1337
  if not isinstance(agent_id, str) or not agent_id.strip():
1090
1338
  raise TaskWorkflowError("TASK_PHASE_RESOURCE_ACTIVE", f"delegated execution status is unknown for {task_id}; preserve the current state")
1091
1339
  agents = _paseo_list(
1092
- ["agent", "ls", "--all", "--global", "--background", "--json", "--label", f"trellis.task_id={task_id}"],
1340
+ ["agent", "ls", "--all", "--global", "--background", "--json", "--label", f"trellis.task_id={record.get('label_task_id', task_id)}"],
1093
1341
  get_repo_root(),
1094
1342
  "TASK_PHASE_RESOURCE_ACTIVE",
1095
1343
  )
1096
1344
  agent = next((item for item in agents if item.get("id") == agent_id), None)
1097
1345
  if agent is None or agent.get("status") != "closed":
1098
1346
  raise TaskWorkflowError("TASK_PHASE_RESOURCE_ACTIVE", f"delegated Paseo agent for {task_id} is not confirmed closed; preserve the current state")
1347
+ worktree_path = record.get("worktree_path")
1348
+ if isinstance(worktree_path, str) and Path(worktree_path).is_dir():
1349
+ if _git_output(["status", "--porcelain", "--untracked-files=all", "--", ".trellis/tasks", ".trellis/.runtime"], Path(worktree_path)):
1350
+ raise TaskWorkflowError("TASK_DELEGATE_TASK_FACTS_CHANGED", "delegated worktree changed protected task facts; preserve evidence and exclude those changes before integration")
1099
1351
 
1100
1352
 
1101
1353
  def cmd_complete_task(args: argparse.Namespace) -> int:
@@ -14,6 +14,7 @@ Usage:
14
14
  python3 task.py workflow <id>|--clear # Set/clear per-task workflow selection
15
15
  python3 task.py phase <action> <task-dir> # Read/update task phase workflow state
16
16
  python3 task.py cancel <task-dir> --reason <text> --evidence-ref <ref> # Cancel task lifecycle
17
+ python3 task.py reopen <task-dir> --reason <text> --evidence-ref <ref> # Restore terminal task to planning
17
18
  python3 task.py set-branch <dir> <branch> # Set git branch
18
19
  python3 task.py set-base-branch <dir> <branch> # Set PR target branch
19
20
  python3 task.py set-scope <dir> <scope> # Set scope for PR title
@@ -79,8 +80,11 @@ from common.task_context import (
79
80
  from common.task_workflow import (
80
81
  TaskWorkflowError,
81
82
  cmd_cancel_task,
83
+ cmd_reopen_task,
82
84
  cmd_complete_task,
83
85
  cmd_delegate_start,
86
+ cmd_delegate_bind,
87
+ cmd_delegate_reconcile,
84
88
  cmd_phase,
85
89
  implementation_start_ready,
86
90
  )
@@ -266,11 +270,11 @@ def cmd_start(args: argparse.Namespace) -> int:
266
270
  run_task_hooks("after_start", task_json_path, repo_root)
267
271
  return 0
268
272
 
273
+ if task_json_path.is_file():
274
+ task_json_path = _record_start_state(task_json_path, repo_root)
275
+ task_dir = task_json_path.parent.relative_to(repo_root).as_posix()
269
276
  active = set_active_task(task_dir, repo_root)
270
277
  if active:
271
- if task_json_path.is_file():
272
- task_json_path = _record_start_state(task_json_path, repo_root)
273
-
274
278
  print(colored(f"✓ Current task set to: {task_json_path.parent.relative_to(repo_root).as_posix()}", Colors.GREEN))
275
279
  print(f"Source: {active.source}")
276
280
  print()
@@ -779,6 +783,7 @@ def main() -> int:
779
783
  p_phase.add_argument("--route-reason", help="Reason when initialization omits one or more phases")
780
784
  p_phase.add_argument("--route-evidence-ref", action="append", help="Evidence supporting an abbreviated initial route")
781
785
  p_phase.add_argument("--target-phase", help="Earlier phase to revisit, or first affected phase for route change")
786
+ p_phase.add_argument("--return-kind", choices=("contract_change", "execution_rework"), default="contract_change", help="Contract change invalidates authorization; execution rework retains a verified unchanged contract")
782
787
  p_phase.add_argument("--artifact-ref", action="append", help="Task-relative file in the current phase output")
783
788
  p_phase.add_argument("--artifact-revision", help="Current artifact_revision required for acceptance")
784
789
  p_phase.add_argument("--result-ref", help="Task result artifact reference")
@@ -794,6 +799,14 @@ def main() -> int:
794
799
 
795
800
  p_delegate = subparsers.add_parser("delegate-start", help="Start an authorized child task in a Paseo worktree")
796
801
  p_delegate.add_argument("child_task", help="Linked child task directory or name")
802
+ p_delegate_bind = subparsers.add_parser("delegate-bind", help="Freeze a linked child contract within current parent authorization")
803
+ p_delegate_bind.add_argument("child_task")
804
+ p_delegate_bind.add_argument("--result-path", help="Assigned artifact under .trellis/delegated-results/")
805
+ p_delegate_reconcile = subparsers.add_parser("delegate-reconcile", help="Inspect or explicitly retire a verified delegated execution")
806
+ p_delegate_reconcile.add_argument("child_task")
807
+ p_delegate_reconcile.add_argument("--execute", action="store_true")
808
+ p_delegate_reconcile.add_argument("--reason")
809
+ p_delegate_reconcile.add_argument("--approval-ref")
797
810
 
798
811
  add_continuity_commands(subparsers)
799
812
 
@@ -803,6 +816,11 @@ def main() -> int:
803
816
  p_cancel.add_argument("--reason", required=True, help="Why the task is cancelled")
804
817
  p_cancel.add_argument("--evidence-ref", action="append", required=True, help="Task-relative cancellation evidence")
805
818
 
819
+ p_reopen = subparsers.add_parser("reopen", help="Recover a terminal task to planning without implementation authorization")
820
+ p_reopen.add_argument("task_dir", help="Task directory or task name")
821
+ p_reopen.add_argument("--reason", required=True, help="Why the same task must continue")
822
+ p_reopen.add_argument("--evidence-ref", action="append", required=True, help="Recovery decision evidence")
823
+
806
824
  # set-branch
807
825
  p_branch = subparsers.add_parser("set-branch", help="Set git branch")
808
826
  p_branch.add_argument("dir", help="Task directory")
@@ -878,8 +896,11 @@ def main() -> int:
878
896
  "workflow": cmd_workflow,
879
897
  "phase": cmd_phase,
880
898
  "delegate-start": cmd_delegate_start,
899
+ "delegate-bind": cmd_delegate_bind,
900
+ "delegate-reconcile": cmd_delegate_reconcile,
881
901
  "complete": cmd_complete_task,
882
902
  "cancel": cmd_cancel_task,
903
+ "reopen": cmd_reopen_task,
883
904
  "set-branch": cmd_set_branch,
884
905
  "set-base-branch": cmd_set_base_branch,
885
906
  "set-scope": cmd_set_scope,
@@ -903,6 +924,9 @@ def main() -> int:
903
924
  return commands[args.command](args)
904
925
  show_usage()
905
926
  return 1
927
+ except TaskWorkflowError as error:
928
+ print(f"{error.code}: {error}", file=sys.stderr)
929
+ return 1
906
930
  except (ValueError, OSError) as error:
907
931
  print(f"TASK_LAYOUT_WITHHELD: {error}", file=sys.stderr)
908
932
  return 1
@@ -0,0 +1,10 @@
1
+ {
2
+ "schema_version": 2,
3
+ "default_mode": "general",
4
+ "modes": {
5
+ "general": { "default_members": 1, "max_concurrency": 8 },
6
+ "advisor": { "default_members": 3, "max_concurrency": 3 },
7
+ "committee": { "default_members": 3, "max_concurrency": 3 }
8
+ },
9
+ "resources": { "max_live_teammates": 8, "non_batch_reserved": 0 }
10
+ }
@@ -30,12 +30,19 @@ python3 ./.trellis/scripts/task.py validate <task>
30
30
  python3 ./.trellis/scripts/task.py start <task>
31
31
  python3 ./.trellis/scripts/task.py complete <task>
32
32
  python3 ./.trellis/scripts/task.py cancel <task> --reason "<reason>" --evidence-ref <reference>
33
+ python3 ./.trellis/scripts/task.py reopen <task> --reason "<reason>" --evidence-ref <reference>
33
34
  ```
34
35
 
35
36
  使用 `phase artifact`、`accept`、`skip`、`wait`、`block`、`resume`、`return`、`route` 和 `authorize` 执行对应的阶段操作。准确参数以 `task.py --help` 为准。跳过阶段必须提供理由和证据;目标探索与需求核对阶段不可省略。
36
37
 
37
38
  只有阶段路线已到达获准的实施入口时,`task.py start` 才会将任务从 `planning` 改为 `in_progress`。分析任务可在交付审查与现场收尾阶段后仍处于 `planning` 时完成。`complete` 将任务移入 `completed/YYYY-MM/<task>`,`cancel` 将任务移入 `cancelled/<task>`,并在同一事务中清除任务的运行时会话指针。
38
39
 
40
+ 工作流阶段回流与原生任务生命周期恢复分别操作。`completed` 或 `cancelled` 任务需要在原线程继续时,先用 `reopen` 恢复到 `planning`,保留终态事实、取消原因和恢复证据,清除旧运行态、实施授权和依赖准入;工作流阶段保持原记录。随后用 `phase return` 回到最早受影响阶段。实现任务重新接受合同、取得新的实施授权并核验准入后,才由 `start` 进入 `in_progress`。`phase return` 不能直接修改终态任务。
41
+
42
+ `phase return` 默认采用 `contract_change`,使受影响阶段接受和实施授权失效。合同不变的局部实施或证据返工可显式使用 `--return-kind execution_rework`,目标限于实施或交付阶段,可重入当前阶段;CLI 核验需求、已接受合同、授权摘要、路线 revision 和已声明依赖准入仍有效后,保留授权,清理目标阶段及下游的接受事实。不能以此修改目标、合同或绕过待决依赖。普通局部修复可直接在当前活动阶段完成,无需为了记录进展而回流。
43
+
44
+ 新接受的 `implementation_contract`(以及省略设计阶段时新授权的合同)必须包含 `## 验收覆盖` 表,表头固定为 `接受条件 | 工作包 | Owner | 验证入口 | 交付证据 | 停止条件`,至少一行且各单元格非空。CLI 负责结构检查,阶段 Skill 负责逐条核对已接受条件的语义覆盖。既有接受和授权不会因本次升级自动失效;再次接受或授权时执行新检查。
45
+
39
46
  旧布局须先预览 `task.py migrate-layout`,核对后执行 `task.py migrate-layout --apply`;旧 `archive/YYYY-MM` 中已完成的任务保留原月份,其他任务按真实状态放置。迁移会同步 JSON/JSONL 机器引用、会话指针和运行态索引,保留任务身份与父子关系,不重授历史授权。未完成事务会阻止普通任务命令,须用 `task.py migrate-layout --recover` 恢复;输入被外部改动时停止并保留现场。路径或产物内容改变导致接受证据失效时,须经原生阶段接受或 continuity 核对重新收敛。
40
47
 
41
48
  在支持原生实施或检查 agent 的平台上,任务上下文清单(`implement.jsonl`、`check.jsonl`)用于提供相关规范和研究资料。只有这些 agent 会读取清单时才需要维护;清单不能替代任务资产或阶段证据。
@@ -1 +1 @@
1
- {"version":3,"file":"asset-backup.d.ts","sourceRoot":"","sources":["../../src/utils/asset-backup.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"asset-backup.d.ts","sourceRoot":"","sources":["../../src/utils/asset-backup.ts"],"names":[],"mappings":"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"}