@pennixrv/trellis 0.7.1 → 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.
- package/README.md +10 -7
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +6 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/ablate.d.ts.map +1 -1
- package/dist/commands/ablate.js +2 -0
- package/dist/commands/ablate.js.map +1 -1
- package/dist/commands/backup.d.ts +4 -0
- package/dist/commands/backup.d.ts.map +1 -0
- package/dist/commands/backup.js +75 -0
- package/dist/commands/backup.js.map +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +705 -681
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/mem.d.ts.map +1 -1
- package/dist/commands/mem.js +49 -7
- package/dist/commands/mem.js.map +1 -1
- package/dist/commands/task-progress.d.ts +1 -0
- package/dist/commands/task-progress.d.ts.map +1 -1
- package/dist/commands/task-progress.js +67 -25
- package/dist/commands/task-progress.js.map +1 -1
- package/dist/commands/teamwork/adapters/acp.d.ts +20 -0
- package/dist/commands/teamwork/adapters/acp.d.ts.map +1 -0
- package/dist/commands/teamwork/adapters/acp.js +268 -0
- package/dist/commands/teamwork/adapters/acp.js.map +1 -0
- package/dist/commands/teamwork/adapters/claude.d.ts +1 -0
- package/dist/commands/teamwork/adapters/claude.d.ts.map +1 -1
- package/dist/commands/teamwork/adapters/claude.js +5 -0
- package/dist/commands/teamwork/adapters/claude.js.map +1 -1
- package/dist/commands/teamwork/adapters/codex.d.ts +3 -2
- package/dist/commands/teamwork/adapters/codex.d.ts.map +1 -1
- package/dist/commands/teamwork/adapters/codex.js +14 -15
- package/dist/commands/teamwork/adapters/codex.js.map +1 -1
- package/dist/commands/teamwork/adapters/index.d.ts +23 -1
- package/dist/commands/teamwork/adapters/index.d.ts.map +1 -1
- package/dist/commands/teamwork/adapters/index.js +163 -24
- package/dist/commands/teamwork/adapters/index.js.map +1 -1
- package/dist/commands/teamwork/agent-loader.d.ts +2 -1
- package/dist/commands/teamwork/agent-loader.d.ts.map +1 -1
- package/dist/commands/teamwork/agent-loader.js +2 -1
- package/dist/commands/teamwork/agent-loader.js.map +1 -1
- package/dist/commands/teamwork/artifact.d.ts +33 -10
- package/dist/commands/teamwork/artifact.d.ts.map +1 -1
- package/dist/commands/teamwork/artifact.js +182 -50
- package/dist/commands/teamwork/artifact.js.map +1 -1
- package/dist/commands/teamwork/batch-reservation.d.ts +16 -0
- package/dist/commands/teamwork/batch-reservation.d.ts.map +1 -0
- package/dist/commands/teamwork/batch-reservation.js +43 -0
- package/dist/commands/teamwork/batch-reservation.js.map +1 -0
- package/dist/commands/teamwork/batch.d.ts +22 -0
- package/dist/commands/teamwork/batch.d.ts.map +1 -0
- package/dist/commands/teamwork/batch.js +394 -0
- package/dist/commands/teamwork/batch.js.map +1 -0
- package/dist/commands/teamwork/context-loader.d.ts +1 -1
- package/dist/commands/teamwork/context-loader.d.ts.map +1 -1
- package/dist/commands/teamwork/context-loader.js +19 -6
- package/dist/commands/teamwork/context-loader.js.map +1 -1
- package/dist/commands/teamwork/coordination.d.ts +88 -0
- package/dist/commands/teamwork/coordination.d.ts.map +1 -0
- package/dist/commands/teamwork/coordination.js +153 -0
- package/dist/commands/teamwork/coordination.js.map +1 -0
- package/dist/commands/teamwork/coordinator.d.ts +17 -3
- package/dist/commands/teamwork/coordinator.d.ts.map +1 -1
- package/dist/commands/teamwork/coordinator.js +772 -289
- package/dist/commands/teamwork/coordinator.js.map +1 -1
- package/dist/commands/teamwork/create.d.ts +1 -0
- package/dist/commands/teamwork/create.d.ts.map +1 -1
- package/dist/commands/teamwork/create.js +2 -1
- package/dist/commands/teamwork/create.js.map +1 -1
- package/dist/commands/teamwork/dev-parse-trace.d.ts +2 -1
- package/dist/commands/teamwork/dev-parse-trace.d.ts.map +1 -1
- package/dist/commands/teamwork/dev-parse-trace.js +2 -0
- package/dist/commands/teamwork/dev-parse-trace.js.map +1 -1
- package/dist/commands/teamwork/foreground-wait.d.ts +6 -0
- package/dist/commands/teamwork/foreground-wait.d.ts.map +1 -0
- package/dist/commands/teamwork/foreground-wait.js +65 -0
- package/dist/commands/teamwork/foreground-wait.js.map +1 -0
- package/dist/commands/teamwork/guard.d.ts +2 -2
- package/dist/commands/teamwork/guard.d.ts.map +1 -1
- package/dist/commands/teamwork/guard.js +23 -5
- package/dist/commands/teamwork/guard.js.map +1 -1
- package/dist/commands/teamwork/index.d.ts.map +1 -1
- package/dist/commands/teamwork/index.js +139 -10
- package/dist/commands/teamwork/index.js.map +1 -1
- package/dist/commands/teamwork/kill.d.ts +2 -0
- package/dist/commands/teamwork/kill.d.ts.map +1 -1
- package/dist/commands/teamwork/kill.js +79 -38
- package/dist/commands/teamwork/kill.js.map +1 -1
- package/dist/commands/teamwork/messages.d.ts.map +1 -1
- package/dist/commands/teamwork/messages.js +3 -0
- package/dist/commands/teamwork/messages.js.map +1 -1
- package/dist/commands/teamwork/plan.d.ts +22 -0
- package/dist/commands/teamwork/plan.d.ts.map +1 -0
- package/dist/commands/teamwork/plan.js +288 -0
- package/dist/commands/teamwork/plan.js.map +1 -0
- package/dist/commands/teamwork/process-identity.d.ts +3 -0
- package/dist/commands/teamwork/process-identity.d.ts.map +1 -0
- package/dist/commands/teamwork/process-identity.js +24 -0
- package/dist/commands/teamwork/process-identity.js.map +1 -0
- package/dist/commands/teamwork/profiles.d.ts +43 -6
- package/dist/commands/teamwork/profiles.d.ts.map +1 -1
- package/dist/commands/teamwork/profiles.js +138 -110
- package/dist/commands/teamwork/profiles.js.map +1 -1
- package/dist/commands/teamwork/prompt.d.ts +4 -0
- package/dist/commands/teamwork/prompt.d.ts.map +1 -0
- package/dist/commands/teamwork/prompt.js +33 -0
- package/dist/commands/teamwork/prompt.js.map +1 -0
- package/dist/commands/teamwork/result.d.ts +54 -0
- package/dist/commands/teamwork/result.d.ts.map +1 -0
- package/dist/commands/teamwork/result.js +94 -0
- package/dist/commands/teamwork/result.js.map +1 -0
- package/dist/commands/teamwork/rm.d.ts.map +1 -1
- package/dist/commands/teamwork/rm.js +64 -64
- package/dist/commands/teamwork/rm.js.map +1 -1
- package/dist/commands/teamwork/run.d.ts +5 -0
- package/dist/commands/teamwork/run.d.ts.map +1 -1
- package/dist/commands/teamwork/run.js +120 -24
- package/dist/commands/teamwork/run.js.map +1 -1
- package/dist/commands/teamwork/runtime.d.ts +9 -0
- package/dist/commands/teamwork/runtime.d.ts.map +1 -0
- package/dist/commands/teamwork/runtime.js +372 -0
- package/dist/commands/teamwork/runtime.js.map +1 -0
- package/dist/commands/teamwork/send.d.ts +1 -0
- package/dist/commands/teamwork/send.d.ts.map +1 -1
- package/dist/commands/teamwork/send.js +2 -1
- package/dist/commands/teamwork/send.js.map +1 -1
- package/dist/commands/teamwork/spawn.d.ts +10 -3
- package/dist/commands/teamwork/spawn.d.ts.map +1 -1
- package/dist/commands/teamwork/spawn.js +42 -27
- package/dist/commands/teamwork/spawn.js.map +1 -1
- package/dist/commands/teamwork/store/lock.js +28 -4
- package/dist/commands/teamwork/store/lock.js.map +1 -1
- package/dist/commands/teamwork/supervisor/inbox.d.ts +1 -0
- package/dist/commands/teamwork/supervisor/inbox.d.ts.map +1 -1
- package/dist/commands/teamwork/supervisor/inbox.js +28 -9
- package/dist/commands/teamwork/supervisor/inbox.js.map +1 -1
- package/dist/commands/teamwork/supervisor/stdout.d.ts.map +1 -1
- package/dist/commands/teamwork/supervisor/stdout.js +2 -3
- package/dist/commands/teamwork/supervisor/stdout.js.map +1 -1
- package/dist/commands/teamwork/supervisor/warning.d.ts.map +1 -1
- package/dist/commands/teamwork/supervisor/warning.js +2 -3
- package/dist/commands/teamwork/supervisor/warning.js.map +1 -1
- package/dist/commands/teamwork/supervisor.d.ts +5 -0
- package/dist/commands/teamwork/supervisor.d.ts.map +1 -1
- package/dist/commands/teamwork/supervisor.js +21 -12
- package/dist/commands/teamwork/supervisor.js.map +1 -1
- package/dist/commands/teamwork/teammate-adapters.d.ts +0 -5
- package/dist/commands/teamwork/teammate-adapters.d.ts.map +1 -1
- package/dist/commands/teamwork/teammate-adapters.js +9 -56
- package/dist/commands/teamwork/teammate-adapters.js.map +1 -1
- package/dist/commands/teamwork/teammate-config.d.ts +5 -1
- package/dist/commands/teamwork/teammate-config.d.ts.map +1 -1
- package/dist/commands/teamwork/teammate-config.js +62 -19
- package/dist/commands/teamwork/teammate-config.js.map +1 -1
- package/dist/commands/teamwork/teammate-launch.d.ts +15 -6
- package/dist/commands/teamwork/teammate-launch.d.ts.map +1 -1
- package/dist/commands/teamwork/teammate-launch.js +129 -22
- package/dist/commands/teamwork/teammate-launch.js.map +1 -1
- package/dist/commands/teamwork/wait.d.ts +2 -0
- package/dist/commands/teamwork/wait.d.ts.map +1 -1
- package/dist/commands/teamwork/wait.js +44 -3
- package/dist/commands/teamwork/wait.js.map +1 -1
- package/dist/commands/uninstall.d.ts.map +1 -1
- package/dist/commands/uninstall.js +92 -84
- package/dist/commands/uninstall.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +635 -592
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +94 -89
- package/dist/commands/workflow.js.map +1 -1
- package/dist/configurators/shared.js +2 -2
- package/dist/configurators/shared.js.map +1 -1
- package/dist/configurators/workflow.d.ts.map +1 -1
- package/dist/configurators/workflow.js +14 -32
- package/dist/configurators/workflow.js.map +1 -1
- package/dist/constants/paths.d.ts +3 -10
- package/dist/constants/paths.d.ts.map +1 -1
- package/dist/constants/paths.js +4 -13
- package/dist/constants/paths.js.map +1 -1
- package/dist/migrations/manifests/0.7.1-beta.0.json +9 -0
- package/dist/migrations/manifests/0.7.1-beta.1.json +186 -0
- package/dist/migrations/manifests/0.7.1-beta.10.json +9 -0
- package/dist/migrations/manifests/0.7.1-beta.11.json +9 -0
- package/dist/migrations/manifests/0.7.1-beta.12.json +9 -0
- package/dist/migrations/manifests/0.7.1-beta.13.json +9 -0
- package/dist/migrations/manifests/0.7.1-beta.14.json +11 -0
- package/dist/migrations/manifests/0.7.1-beta.15.json +9 -0
- package/dist/migrations/manifests/0.7.1-beta.16.json +9 -0
- package/dist/migrations/manifests/0.7.1-beta.17.json +9 -0
- package/dist/migrations/manifests/0.7.1-beta.18.json +9 -0
- package/dist/migrations/manifests/0.7.1-beta.19.json +9 -0
- package/dist/migrations/manifests/0.7.1-beta.2.json +522 -0
- package/dist/migrations/manifests/0.7.1-beta.3.json +9 -0
- package/dist/migrations/manifests/0.7.1-beta.4.json +9 -0
- package/dist/migrations/manifests/0.7.1-beta.5.json +9 -0
- package/dist/migrations/manifests/0.7.1-beta.6.json +10 -0
- package/dist/migrations/manifests/0.7.1-beta.7.json +8 -0
- package/dist/migrations/manifests/0.7.1-beta.8.json +481 -0
- package/dist/migrations/manifests/0.7.1-beta.9.json +191 -0
- package/dist/migrations/manifests/0.7.2-beta.0.json +11 -0
- package/dist/templates/claude/hooks/statusline.py +9 -4
- package/dist/templates/codex/hooks/session-start.py +14 -11
- package/dist/templates/codex/hooks.json +10 -0
- package/dist/templates/common/bundled-skills/trellis-decision-space-construction-and-merging/workflow.md +2 -0
- package/dist/templates/common/bundled-skills/trellis-goal-exploration-and-requirements/workflow.md +2 -0
- package/dist/templates/common/bundled-skills/trellis-meta/SKILL.md +1 -1
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/add-project-local-conventions.md +1 -1
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-context-loading.md +5 -5
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-task-lifecycle.md +3 -2
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/overview.md +1 -1
- package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/context-injection.md +1 -1
- package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/multi-agent-teamwork.md +14 -2
- package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/task-system.md +8 -2
- package/dist/templates/common/bundled-skills/trellis-session-insight/references/cli-quick-reference.md +2 -1
- package/dist/templates/common/bundled-skills/trellis-session-insight/references/triggering-patterns.md +2 -2
- package/dist/templates/common/bundled-skills/trellis-solution-design-and-convergence/workflow.md +8 -0
- package/dist/templates/common/bundled-skills/trellis-spec-bootstrap/references/mcp-setup.md +4 -2
- package/dist/templates/common/bundled-skills/trellis-spec-bootstrap/references/spec-task-planning.md +2 -2
- package/dist/templates/common/bundled-skills/trellis-task-implementation/workflow.md +6 -0
- package/dist/templates/common/bundled-skills/trellis-teamwork/SKILL.md +18 -3
- package/dist/templates/common/bundled-skills/trellis-teamwork/references/coordination-modes.md +34 -3
- package/dist/templates/common/bundled-skills/trellis-teamwork/references/teammate-configuration-matrix.md +37 -26
- package/dist/templates/common/bundled-skills/trellis-teamwork/references/teammate-processes.md +128 -14
- package/dist/templates/common/bundled-skills/trellis-teamwork/references/teamwork-plan-construction.md +152 -20
- package/dist/templates/common/commands/start.md +2 -2
- package/dist/templates/common/skills/before-dev.md +1 -4
- package/dist/templates/common/skills/break-loop.md +7 -18
- package/dist/templates/common/skills/check.md +7 -11
- package/dist/templates/common/skills/{brainstorm.md → request-intake.md} +5 -3
- package/dist/templates/common/skills/update-spec.md +5 -7
- package/dist/templates/copilot/hooks/session-start.py +14 -11
- package/dist/templates/copilot/index.d.ts +1 -6
- package/dist/templates/copilot/index.d.ts.map +1 -1
- package/dist/templates/copilot/index.js +1 -14
- package/dist/templates/copilot/index.js.map +1 -1
- package/dist/templates/extract.js +4 -0
- package/dist/templates/extract.js.map +1 -1
- package/dist/templates/markdown/spec/backend/index.md.txt +4 -35
- package/dist/templates/markdown/spec/frontend/index.md.txt +4 -36
- package/dist/templates/omp/extensions/trellis/index.ts.txt +2 -2
- package/dist/templates/opencode/lib/session-utils.js +301 -209
- package/dist/templates/pi/extensions/trellis/index.ts.txt +17 -1
- package/dist/templates/shared-hooks/guard-teamwork-wait.py +53 -0
- package/dist/templates/shared-hooks/index.d.ts +1 -1
- package/dist/templates/shared-hooks/index.d.ts.map +1 -1
- package/dist/templates/shared-hooks/index.js +1 -0
- package/dist/templates/shared-hooks/index.js.map +1 -1
- package/dist/templates/shared-hooks/inject-workflow-state.py +8 -3
- package/dist/templates/shared-hooks/session-start.py +16 -12
- package/dist/templates/snow/SNOW.md +1 -1
- package/dist/templates/trellis/agents/delegated-worktree-primary-agent.md +9 -0
- package/dist/templates/trellis/agents/profiles/architecture.md +9 -0
- package/dist/templates/trellis/agents/profiles/diagnosis.md +9 -0
- package/dist/templates/trellis/agents/profiles/exploration.md +9 -0
- package/dist/templates/trellis/agents/profiles/research.md +9 -0
- package/dist/templates/trellis/agents/profiles/review.md +9 -0
- package/dist/templates/trellis/agents/profiles/verification.md +9 -0
- package/dist/templates/trellis/agents/teammate-profiles.json +56 -4
- package/dist/templates/trellis/agents/teammate.md +4 -52
- package/dist/templates/trellis/config.yaml +2 -5
- package/dist/templates/trellis/index.d.ts +4 -0
- package/dist/templates/trellis/index.d.ts.map +1 -1
- package/dist/templates/trellis/index.js +22 -4
- package/dist/templates/trellis/index.js.map +1 -1
- package/dist/templates/trellis/scripts/add_session.py +1 -1
- package/dist/templates/trellis/scripts/common/active_task.py +3 -47
- package/dist/templates/trellis/scripts/common/paths.py +9 -25
- package/dist/templates/trellis/scripts/common/safe_commit.py +8 -8
- package/dist/templates/trellis/scripts/common/task_context.py +11 -51
- package/dist/templates/trellis/scripts/common/task_continuity.py +129 -60
- package/dist/templates/trellis/scripts/common/task_layout.py +80 -0
- package/dist/templates/trellis/scripts/common/task_move.py +345 -0
- package/dist/templates/trellis/scripts/common/task_store.py +35 -57
- package/dist/templates/trellis/scripts/common/task_utils.py +23 -19
- package/dist/templates/trellis/scripts/common/task_workflow.py +339 -46
- package/dist/templates/trellis/scripts/common/tasks.py +6 -5
- package/dist/templates/trellis/scripts/hooks/linear_sync.py +13 -3
- package/dist/templates/trellis/scripts/task.py +71 -24
- package/dist/templates/trellis/teamwork.json +10 -0
- package/dist/templates/trellis/workflow.md +11 -2
- package/dist/utils/ablation-store.d.ts +1 -1
- package/dist/utils/ablation-store.d.ts.map +1 -1
- package/dist/utils/ablation-store.js +58 -24
- package/dist/utils/ablation-store.js.map +1 -1
- package/dist/utils/asset-backup.d.ts +138 -0
- package/dist/utils/asset-backup.d.ts.map +1 -0
- package/dist/utils/asset-backup.js +513 -0
- package/dist/utils/asset-backup.js.map +1 -0
- package/dist/utils/atomic-write.d.ts +1 -1
- package/dist/utils/atomic-write.d.ts.map +1 -1
- package/dist/utils/atomic-write.js +6 -3
- package/dist/utils/atomic-write.js.map +1 -1
- package/dist/utils/manifest-prune.d.ts +4 -1
- package/dist/utils/manifest-prune.d.ts.map +1 -1
- package/dist/utils/manifest-prune.js +17 -6
- package/dist/utils/manifest-prune.js.map +1 -1
- package/dist/utils/template-fetcher.d.ts +4 -19
- package/dist/utils/template-fetcher.d.ts.map +1 -1
- package/dist/utils/template-fetcher.js +9 -45
- package/dist/utils/template-fetcher.js.map +1 -1
- package/dist/utils/template-hash.d.ts +5 -4
- package/dist/utils/template-hash.d.ts.map +1 -1
- package/dist/utils/template-hash.js +24 -17
- package/dist/utils/template-hash.js.map +1 -1
- package/package.json +3 -2
- package/dist/templates/copilot/prompts/before-dev.prompt.md +0 -39
- package/dist/templates/copilot/prompts/brainstorm.prompt.md +0 -13
- package/dist/templates/copilot/prompts/break-loop.prompt.md +0 -129
- package/dist/templates/copilot/prompts/check-cross-layer.prompt.md +0 -170
- package/dist/templates/copilot/prompts/check.prompt.md +0 -110
- package/dist/templates/copilot/prompts/create-command.prompt.md +0 -116
- package/dist/templates/copilot/prompts/finish-work.prompt.md +0 -22
- package/dist/templates/copilot/prompts/integrate-skill.prompt.md +0 -223
- package/dist/templates/copilot/prompts/onboard.prompt.md +0 -362
- package/dist/templates/copilot/prompts/record-session.prompt.md +0 -58
- package/dist/templates/copilot/prompts/start.prompt.md +0 -65
- 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
|
|
@@ -61,11 +62,14 @@ def _task_dir(value: str) -> Path:
|
|
|
61
62
|
return task_dir
|
|
62
63
|
|
|
63
64
|
|
|
64
|
-
def _load(task_dir: Path) -> tuple[dict, dict]:
|
|
65
|
+
def _load(task_dir: Path, *, initialize_meta: bool = False) -> tuple[dict, dict]:
|
|
65
66
|
task_file = task_dir / FILE_TASK_JSON
|
|
66
67
|
data, reason = read_json_checked(task_file)
|
|
67
68
|
if data is None:
|
|
68
69
|
raise TaskWorkflowError("TASK_PHASE_TASK_INVALID", f"cannot read {task_file}: {reason}")
|
|
70
|
+
# Explicit initialization may adopt legacy records; malformed metadata still fails.
|
|
71
|
+
if initialize_meta and "meta" not in data:
|
|
72
|
+
data["meta"] = {}
|
|
69
73
|
meta = data.get("meta")
|
|
70
74
|
if not isinstance(meta, dict):
|
|
71
75
|
raise TaskWorkflowError("TASK_PHASE_TASK_INVALID", "task.meta must be an object")
|
|
@@ -130,6 +134,10 @@ def implementation_start_ready(data: dict, task_dir: Path) -> bool:
|
|
|
130
134
|
phase_ids = state.get("phase_ids")
|
|
131
135
|
if not isinstance(phase_ids, list) or PHASE_IDS[3] not in phase_ids:
|
|
132
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
|
|
133
141
|
if state.get("task_kind") not in {"implementation", "combined"}:
|
|
134
142
|
return False
|
|
135
143
|
authorization = state.get("implementation_authorization")
|
|
@@ -145,6 +153,7 @@ def implementation_start_ready(data: dict, task_dir: Path) -> bool:
|
|
|
145
153
|
):
|
|
146
154
|
return False
|
|
147
155
|
_require_current_requirements_confirmation(task_dir, state)
|
|
156
|
+
_require_current_accepted_artifacts(task_dir, state)
|
|
148
157
|
if PHASE_IDS[2] not in phase_ids:
|
|
149
158
|
skip_records = state.get("skip_records")
|
|
150
159
|
skipped_design = skip_records.get(PHASE_IDS[2]) if isinstance(skip_records, dict) else None
|
|
@@ -176,7 +185,8 @@ def _save(task_file: Path, data: dict) -> None:
|
|
|
176
185
|
|
|
177
186
|
@contextlib.contextmanager
|
|
178
187
|
def _task_lock(task_dir: Path):
|
|
179
|
-
lock_file =
|
|
188
|
+
lock_file = get_repo_root() / ".trellis/.runtime/task-phase" / f"{task_dir.name}.lock"
|
|
189
|
+
lock_file.parent.mkdir(parents=True, exist_ok=True)
|
|
180
190
|
with lock_file.open("a+b") as handle:
|
|
181
191
|
if os.name == "nt":
|
|
182
192
|
import msvcrt
|
|
@@ -223,20 +233,23 @@ def _phase(state: dict, phase_id: str | None = None) -> tuple[str, dict]:
|
|
|
223
233
|
return current, record
|
|
224
234
|
|
|
225
235
|
|
|
226
|
-
def
|
|
227
|
-
if PHASE_IDS[0] not in state["phase_ids"]:
|
|
228
|
-
return
|
|
236
|
+
def _is_withdrawal_route(state: dict) -> bool:
|
|
229
237
|
requirements = state["phases"].get(PHASE_IDS[0], {})
|
|
230
238
|
withdrawal = state.get("return_record")
|
|
231
239
|
skip_records = state.get("skip_records")
|
|
232
240
|
skipped_requirements = skip_records.get(PHASE_IDS[0]) if isinstance(skip_records, dict) else None
|
|
233
|
-
|
|
241
|
+
return (
|
|
234
242
|
state["phase_ids"] == [PHASE_IDS[0], PHASE_IDS[4]]
|
|
235
|
-
and state.get("current_phase_id")
|
|
243
|
+
and state.get("current_phase_id") in {PHASE_IDS[4], None}
|
|
236
244
|
and isinstance(requirements, dict)
|
|
237
|
-
and
|
|
238
|
-
|
|
239
|
-
|
|
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
|
+
)
|
|
240
253
|
and isinstance(withdrawal, dict)
|
|
241
254
|
and isinstance(withdrawal.get("from"), str)
|
|
242
255
|
and withdrawal["from"] in PHASE_ID_SET
|
|
@@ -244,7 +257,14 @@ def _require_current_requirements_confirmation(task_dir: Path, state: dict) -> N
|
|
|
244
257
|
and withdrawal.get("task_route_revision") == state.get("task_route_revision")
|
|
245
258
|
and isinstance(withdrawal.get("evidence_refs"), list)
|
|
246
259
|
and bool(withdrawal["evidence_refs"])
|
|
247
|
-
)
|
|
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):
|
|
248
268
|
return
|
|
249
269
|
acceptance = requirements.get("acceptance") if isinstance(requirements, dict) else None
|
|
250
270
|
if (
|
|
@@ -280,6 +300,7 @@ def _invalidate_from(state: dict, first_phase: str, keep_first: bool = False) ->
|
|
|
280
300
|
for phase_id in state["phase_ids"]:
|
|
281
301
|
if PHASE_IDS.index(phase_id) < first_index:
|
|
282
302
|
continue
|
|
303
|
+
state["skip_records"].pop(phase_id, None)
|
|
283
304
|
record = state["phases"][phase_id]
|
|
284
305
|
if keep_first and phase_id == first_phase:
|
|
285
306
|
record["status"] = "active"
|
|
@@ -327,6 +348,24 @@ def _artifact_digest(task_dir: Path, refs: list[str]) -> str:
|
|
|
327
348
|
return digest.hexdigest()
|
|
328
349
|
|
|
329
350
|
|
|
351
|
+
def _require_current_accepted_artifacts(task_dir: Path, state: dict) -> None:
|
|
352
|
+
for phase_id in state["phase_ids"]:
|
|
353
|
+
record = state["phases"][phase_id]
|
|
354
|
+
if record.get("status") != "accepted":
|
|
355
|
+
continue
|
|
356
|
+
refs = record.get("artifact_refs")
|
|
357
|
+
if not isinstance(refs, list) or any(not isinstance(ref, str) for ref in refs):
|
|
358
|
+
raise TaskWorkflowError("TASK_PHASE_TASK_INVALID", f"accepted artifact references are invalid: {phase_id}")
|
|
359
|
+
if _artifact_digest(task_dir, refs) != record.get("artifact_revision"):
|
|
360
|
+
raise TaskWorkflowError("TASK_PHASE_STALE_ARTIFACT", f"accepted artifacts changed for {phase_id}; return to the affected phase and review the current candidate again")
|
|
361
|
+
authorization = state.get("implementation_authorization")
|
|
362
|
+
if authorization is not None:
|
|
363
|
+
if not isinstance(authorization, dict) or not isinstance(authorization.get("contract_ref"), str):
|
|
364
|
+
raise TaskWorkflowError("TASK_PHASE_TASK_INVALID", "implementation authorization has no contract reference")
|
|
365
|
+
if _artifact_digest(task_dir, [authorization["contract_ref"]]) != authorization.get("contract_revision"):
|
|
366
|
+
raise TaskWorkflowError("TASK_PHASE_STALE_ARTIFACT", "implementation contract changed after authorization; review and authorize its current content before continuing")
|
|
367
|
+
|
|
368
|
+
|
|
330
369
|
def _validate_route(value: str | None) -> list[str]:
|
|
331
370
|
raw = _required_text(value, "--phase-ids")
|
|
332
371
|
phases = raw.split(",")
|
|
@@ -417,6 +456,31 @@ def _show(state: dict) -> int:
|
|
|
417
456
|
return 0
|
|
418
457
|
|
|
419
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
|
+
|
|
420
484
|
def _update(args: argparse.Namespace, task_dir: Path, data: dict, state: dict) -> int:
|
|
421
485
|
action = args.phase_action
|
|
422
486
|
child_dispositions: dict[str, str] = {}
|
|
@@ -427,14 +491,21 @@ def _update(args: argparse.Namespace, task_dir: Path, data: dict, state: dict) -
|
|
|
427
491
|
raise TaskWorkflowError("TASK_PHASE_TASK_TERMINAL", f"task is already {data['status']}")
|
|
428
492
|
|
|
429
493
|
_expected_route(state, args.expected_route_revision)
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
494
|
+
reviewed_delivery = (
|
|
495
|
+
state.get("current_phase_id") is None
|
|
496
|
+
and action in {"return", "route"}
|
|
497
|
+
and state["phases"].get(PHASE_IDS[4], {}).get("status") == "accepted"
|
|
498
|
+
)
|
|
499
|
+
if reviewed_delivery:
|
|
500
|
+
current, record = PHASE_IDS[4], state["phases"][PHASE_IDS[4]]
|
|
501
|
+
else:
|
|
502
|
+
current, record = _phase(state)
|
|
503
|
+
accepted_design_return = action == "return" and current == PHASE_IDS[2] and record.get("status") == "accepted"
|
|
433
504
|
returning_to_requirements = action == "return" and args.target_phase == PHASE_IDS[0]
|
|
434
505
|
rerouting_to_requirements = action == "route" and args.target_phase == PHASE_IDS[0]
|
|
435
506
|
if current != PHASE_IDS[0] and not returning_to_requirements and not rerouting_to_requirements:
|
|
436
507
|
_require_current_requirements_confirmation(task_dir, state)
|
|
437
|
-
if action not in {"show", "resume", "route", "authorize", "admission"} and record.get("status") != "active":
|
|
508
|
+
if action not in {"show", "resume", "route", "authorize", "admission"} and record.get("status") != "active" and not reviewed_delivery and not accepted_design_return:
|
|
438
509
|
raise TaskWorkflowError("TASK_PHASE_INVALID_TRANSITION", "only the active phase can be changed")
|
|
439
510
|
|
|
440
511
|
if action == "artifact":
|
|
@@ -481,6 +552,7 @@ def _update(args: argparse.Namespace, task_dir: Path, data: dict, state: dict) -
|
|
|
481
552
|
state["skip_records"][current] = {"reason": reason, "evidence_refs": evidence, "task_route_revision": state["task_route_revision"]}
|
|
482
553
|
_advance(state, current)
|
|
483
554
|
elif action == "accept":
|
|
555
|
+
_require_current_accepted_artifacts(task_dir, state)
|
|
484
556
|
artifact_revision = _required_text(args.artifact_revision, "--artifact-revision")
|
|
485
557
|
current_revision = _artifact_digest(task_dir, record["artifact_refs"])
|
|
486
558
|
if artifact_revision != record["artifact_revision"] or current_revision != record["artifact_revision"]:
|
|
@@ -495,6 +567,8 @@ def _update(args: argparse.Namespace, task_dir: Path, data: dict, state: dict) -
|
|
|
495
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")
|
|
496
568
|
if result_ref not in record["artifact_refs"]:
|
|
497
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)
|
|
498
572
|
if current == PHASE_IDS[3]:
|
|
499
573
|
authorization = state.get("implementation_authorization")
|
|
500
574
|
if not isinstance(authorization, dict) or authorization.get("task_route_revision") != state["task_route_revision"]:
|
|
@@ -505,8 +579,7 @@ def _update(args: argparse.Namespace, task_dir: Path, data: dict, state: dict) -
|
|
|
505
579
|
if outcome != args.closeout_disposition:
|
|
506
580
|
raise TaskWorkflowError("TASK_PHASE_INVALID_INPUT", "Delivery Review and On-Site Closeout Phase outcome must match --closeout-disposition")
|
|
507
581
|
if args.closeout_disposition == "cancelled":
|
|
508
|
-
|
|
509
|
-
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):
|
|
510
583
|
raise TaskWorkflowError("TASK_PHASE_USER_CONFIRMATION_REQUIRED", "cancellation closeout requires a user-confirmed route to Delivery Review and On-Site Closeout Phase")
|
|
511
584
|
child_dispositions = _validate_child_dispositions(
|
|
512
585
|
task_dir, data, args.closeout_disposition, args.child_disposition
|
|
@@ -532,14 +605,38 @@ def _update(args: argparse.Namespace, task_dir: Path, data: dict, state: dict) -
|
|
|
532
605
|
_advance(state, current)
|
|
533
606
|
elif action == "return":
|
|
534
607
|
target = _required_text(args.target_phase, "--target-phase")
|
|
535
|
-
|
|
608
|
+
return_kind = args.return_kind
|
|
609
|
+
execution_rework = return_kind == "execution_rework"
|
|
610
|
+
if target not in state["phase_ids"] or (
|
|
611
|
+
state["phase_ids"].index(target) >= state["phase_ids"].index(current)
|
|
612
|
+
and not ((reviewed_delivery or accepted_design_return or execution_rework) and target == current)
|
|
613
|
+
):
|
|
536
614
|
raise TaskWorkflowError("TASK_PHASE_INVALID_TRANSITION", "return target must be an earlier phase in the current route")
|
|
537
615
|
reason = _required_text(args.reason, "--reason")
|
|
538
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")
|
|
539
633
|
state["input_revision"] = _increment(state["input_revision"])
|
|
540
634
|
_invalidate_from(state, target, keep_first=True)
|
|
635
|
+
if execution_rework:
|
|
636
|
+
state["implementation_authorization"] = authorization
|
|
637
|
+
state["route_dependency_admission"] = admission
|
|
541
638
|
state["phases"][target]["input_revision"] = state["input_revision"]
|
|
542
|
-
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"]}
|
|
543
640
|
elif action == "route":
|
|
544
641
|
phases = _validate_route(args.phase_ids)
|
|
545
642
|
task_kind = args.task_kind if args.task_kind is not None else state["task_kind"]
|
|
@@ -564,7 +661,7 @@ def _update(args: argparse.Namespace, task_dir: Path, data: dict, state: dict) -
|
|
|
564
661
|
and phases == [PHASE_IDS[0], PHASE_IDS[4]]
|
|
565
662
|
and bool(evidence)
|
|
566
663
|
)
|
|
567
|
-
if not changed and not classification_changed and target == current:
|
|
664
|
+
if not changed and not classification_changed and target == current and not reviewed_delivery:
|
|
568
665
|
raise TaskWorkflowError("TASK_PHASE_INVALID_ROUTE", "the updated route and target are unchanged")
|
|
569
666
|
earliest_change = min((PHASE_IDS.index(phase) for phase in changed), default=PHASE_IDS.index(target))
|
|
570
667
|
if target not in PHASE_ID_SET or (PHASE_IDS.index(target) > earliest_change and not withdrawal_route):
|
|
@@ -604,7 +701,11 @@ def _update(args: argparse.Namespace, task_dir: Path, data: dict, state: dict) -
|
|
|
604
701
|
for phase_id, phase_record in state["phases"].items():
|
|
605
702
|
if phase_id not in old_phases:
|
|
606
703
|
phase_record["input_revision"] = state["input_revision"]
|
|
607
|
-
state["skip_records"] = {
|
|
704
|
+
state["skip_records"] = {
|
|
705
|
+
phase_id: {**skip, "task_route_revision": state["task_route_revision"]}
|
|
706
|
+
for phase_id, skip in state["skip_records"].items()
|
|
707
|
+
if phase_id in phases and state["phases"][phase_id].get("status") == "skipped"
|
|
708
|
+
}
|
|
608
709
|
for phase_id in omitted:
|
|
609
710
|
state["skip_records"][phase_id] = {"reason": reason, "evidence_refs": evidence, "task_route_revision": state["task_route_revision"]}
|
|
610
711
|
if withdrawal_route and state["phases"][PHASE_IDS[0]].get("status") != "accepted":
|
|
@@ -647,7 +748,8 @@ def _update(args: argparse.Namespace, task_dir: Path, data: dict, state: dict) -
|
|
|
647
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")
|
|
648
749
|
elif contract_ref not in state.get("skip_records", {}).get(PHASE_IDS[2], {}).get("evidence_refs", []):
|
|
649
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")
|
|
650
|
-
|
|
751
|
+
_require_acceptance_coverage(task_dir, contract_ref)
|
|
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"]}
|
|
651
753
|
if current == PHASE_IDS[2]:
|
|
652
754
|
_advance(state, current)
|
|
653
755
|
else:
|
|
@@ -662,7 +764,7 @@ def cmd_phase(args: argparse.Namespace) -> int:
|
|
|
662
764
|
try:
|
|
663
765
|
task_dir = _task_dir(args.task_dir)
|
|
664
766
|
with _task_lock(task_dir):
|
|
665
|
-
data, meta = _load(task_dir)
|
|
767
|
+
data, meta = _load(task_dir, initialize_meta=args.phase_action == "init")
|
|
666
768
|
if args.phase_action == "init":
|
|
667
769
|
return _init(args, task_dir, data, meta)
|
|
668
770
|
return _update(args, task_dir, data, _state(meta))
|
|
@@ -707,6 +809,56 @@ def _paseo_list(args: list[str], cwd: Path, code: str) -> list[dict]:
|
|
|
707
809
|
return value
|
|
708
810
|
|
|
709
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
|
+
|
|
710
862
|
def cmd_delegate_start(args: argparse.Namespace) -> int:
|
|
711
863
|
try:
|
|
712
864
|
repo_root = get_repo_root().resolve()
|
|
@@ -722,6 +874,11 @@ def cmd_delegate_start(args: argparse.Namespace) -> int:
|
|
|
722
874
|
child_data, child_meta = _load(child_dir)
|
|
723
875
|
if child_data.get("parent") != parent_dir.name or child_dir.name not in parent_data.get("children", []):
|
|
724
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")
|
|
725
882
|
if parent_data.get("status") != "in_progress" or not implementation_start_ready(parent_data, parent_dir):
|
|
726
883
|
raise TaskWorkflowError("TASK_DELEGATE_IMPLEMENTATION_NOT_AUTHORIZED", "parent task is not at an authorized Task Implementation and Test Verification Phase entry")
|
|
727
884
|
|
|
@@ -775,6 +932,7 @@ def cmd_delegate_start(args: argparse.Namespace) -> int:
|
|
|
775
932
|
raise TaskWorkflowError("TASK_DELEGATE_PASEO_UNAVAILABLE", f"could not run Paseo: {error}") from error
|
|
776
933
|
if version.returncode != 0:
|
|
777
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")
|
|
778
936
|
role_path = repo_root / ".trellis" / "agents" / "delegated-worktree-primary-agent.md"
|
|
779
937
|
try:
|
|
780
938
|
role = role_path.read_text(encoding="utf-8")
|
|
@@ -785,6 +943,16 @@ def cmd_delegate_start(args: argparse.Namespace) -> int:
|
|
|
785
943
|
if not isinstance(authorization, dict) or not isinstance(authorization.get("approval_ref"), str):
|
|
786
944
|
raise TaskWorkflowError("TASK_DELEGATE_AUTHORIZATION_INVALID", "current implementation authorization is invalid")
|
|
787
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")
|
|
788
956
|
previous = child_meta.get("delegated_worktree_execution_record")
|
|
789
957
|
reuse_workspace = False
|
|
790
958
|
workspace_id = None
|
|
@@ -808,7 +976,7 @@ def cmd_delegate_start(args: argparse.Namespace) -> int:
|
|
|
808
976
|
previous_worktree = Path(previous_worktree_path).expanduser().resolve()
|
|
809
977
|
agents = _paseo_list([
|
|
810
978
|
"agent", "ls", "--all", "--global", "--background", "--json",
|
|
811
|
-
"--label", f"trellis.task_id={child_dir.name}",
|
|
979
|
+
"--label", f"trellis.task_id={previous.get('label_task_id', child_dir.name)}",
|
|
812
980
|
], repo_root, "TASK_DELEGATE_RECONCILIATION_REQUIRED")
|
|
813
981
|
for agent in agents:
|
|
814
982
|
agent_id = agent.get("id")
|
|
@@ -859,9 +1027,14 @@ def cmd_delegate_start(args: argparse.Namespace) -> int:
|
|
|
859
1027
|
"workspace_id": workspace_id,
|
|
860
1028
|
"worktree_path": str(worktree) if worktree else None,
|
|
861
1029
|
"agent_id": None,
|
|
1030
|
+
"label_task_id": child_dir.name,
|
|
1031
|
+
"role_sha256": actual["role_sha256"],
|
|
1032
|
+
"result_path": actual["result_path"],
|
|
862
1033
|
"status": "starting",
|
|
863
1034
|
"started_at": datetime.now(timezone.utc).isoformat(),
|
|
864
1035
|
}
|
|
1036
|
+
if previous is not None:
|
|
1037
|
+
child_meta.setdefault("delegated_worktree_execution_history", []).append(previous)
|
|
865
1038
|
child_meta["delegated_worktree_execution_record"] = record
|
|
866
1039
|
_save(child_dir / FILE_TASK_JSON, child_data)
|
|
867
1040
|
|
|
@@ -902,6 +1075,7 @@ def cmd_delegate_start(args: argparse.Namespace) -> int:
|
|
|
902
1075
|
"the parent authorization and Git baseline are execution metadata. Do not accept task phases or change task lifecycle state.\n\n"
|
|
903
1076
|
f"Parent task: {parent_dir.name}\nParent route revision: {state['task_route_revision']}\n"
|
|
904
1077
|
f"Authorized contract: prd.md\nBranch: {branch}\nBase commit: {base_commit}\n\n"
|
|
1078
|
+
f"Assigned result artifact: {actual['result_path']}\n"
|
|
905
1079
|
f"{role}\n\n## Child task prd.md\n{contract}"
|
|
906
1080
|
)
|
|
907
1081
|
agent = _paseo_json([
|
|
@@ -939,6 +1113,7 @@ def cmd_cancel_task(args: argparse.Namespace) -> int:
|
|
|
939
1113
|
readiness_error = task_closeout_readiness(data, "cancelled")
|
|
940
1114
|
if readiness_error:
|
|
941
1115
|
raise TaskWorkflowError("TASK_PHASE_CLOSEOUT_REQUIRED", readiness_error)
|
|
1116
|
+
_require_current_accepted_artifacts(task_dir, _state(meta))
|
|
942
1117
|
_validate_child_dispositions(task_dir, data, "cancelled", None, from_acceptance=True)
|
|
943
1118
|
_assert_delegated_resources_closed(task_dir, data)
|
|
944
1119
|
meta["task_cancellation"] = {
|
|
@@ -947,7 +1122,8 @@ def cmd_cancel_task(args: argparse.Namespace) -> int:
|
|
|
947
1122
|
}
|
|
948
1123
|
data["status"] = "cancelled"
|
|
949
1124
|
data["completedAt"] = None
|
|
950
|
-
|
|
1125
|
+
from .task_move import relocate_task
|
|
1126
|
+
task_dir = relocate_task(get_repo_root(), task_dir, data)
|
|
951
1127
|
from .active_task import clear_task_from_sessions
|
|
952
1128
|
from .task_continuity import clear_task_runtime_records
|
|
953
1129
|
|
|
@@ -961,6 +1137,56 @@ def cmd_cancel_task(args: argparse.Namespace) -> int:
|
|
|
961
1137
|
return 1
|
|
962
1138
|
|
|
963
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
|
+
|
|
964
1190
|
def task_closeout_readiness(data: dict, disposition: str) -> str | None:
|
|
965
1191
|
if "meta" in data and not isinstance(data["meta"], dict):
|
|
966
1192
|
return "task metadata is malformed"
|
|
@@ -982,8 +1208,7 @@ def task_closeout_readiness(data: dict, disposition: str) -> str | None:
|
|
|
982
1208
|
if state.get("closeout_disposition") != disposition:
|
|
983
1209
|
return f"Delivery Review and On-Site Closeout Phase disposition is {state.get('closeout_disposition')}; expected {disposition}"
|
|
984
1210
|
if disposition == "cancelled":
|
|
985
|
-
|
|
986
|
-
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):
|
|
987
1212
|
return "cancellation requires a user-confirmed route to Delivery Review and On-Site Closeout Phase"
|
|
988
1213
|
return None
|
|
989
1214
|
|
|
@@ -1033,27 +1258,96 @@ def _validate_child_dispositions(
|
|
|
1033
1258
|
return choices
|
|
1034
1259
|
|
|
1035
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
|
+
|
|
1036
1322
|
def _assert_delegated_resources_closed(task_dir: Path, data: dict) -> None:
|
|
1037
|
-
repo_root = get_repo_root()
|
|
1038
1323
|
children = data.get("children", [])
|
|
1039
1324
|
if not isinstance(children, list):
|
|
1040
1325
|
raise TaskWorkflowError("TASK_PHASE_RESOURCE_ACTIVE", "linked child task list is malformed")
|
|
1041
1326
|
for task_id in [task_dir.name, *children]:
|
|
1042
1327
|
_, child_meta = _load(_task_dir(task_id))
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
)
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1328
|
+
_assert_delegated_record_closed(task_id, child_meta.get("delegated_worktree_execution_record"))
|
|
1329
|
+
|
|
1330
|
+
|
|
1331
|
+
def _assert_delegated_record_closed(task_id: str, record) -> None:
|
|
1332
|
+
if record is None:
|
|
1333
|
+
return
|
|
1334
|
+
if isinstance(record, dict) and record.get("status") == "retired" and isinstance(record.get("closure_evidence"), dict):
|
|
1335
|
+
return
|
|
1336
|
+
agent_id = record.get("agent_id") if isinstance(record, dict) else None
|
|
1337
|
+
if not isinstance(agent_id, str) or not agent_id.strip():
|
|
1338
|
+
raise TaskWorkflowError("TASK_PHASE_RESOURCE_ACTIVE", f"delegated execution status is unknown for {task_id}; preserve the current state")
|
|
1339
|
+
agents = _paseo_list(
|
|
1340
|
+
["agent", "ls", "--all", "--global", "--background", "--json", "--label", f"trellis.task_id={record.get('label_task_id', task_id)}"],
|
|
1341
|
+
get_repo_root(),
|
|
1342
|
+
"TASK_PHASE_RESOURCE_ACTIVE",
|
|
1343
|
+
)
|
|
1344
|
+
agent = next((item for item in agents if item.get("id") == agent_id), None)
|
|
1345
|
+
if agent is None or agent.get("status") != "closed":
|
|
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")
|
|
1057
1351
|
|
|
1058
1352
|
|
|
1059
1353
|
def cmd_complete_task(args: argparse.Namespace) -> int:
|
|
@@ -1068,9 +1362,7 @@ def cmd_complete_task(args: argparse.Namespace) -> int:
|
|
|
1068
1362
|
disposition = state.get("closeout_disposition")
|
|
1069
1363
|
if disposition not in {"delivery_ready", "no_go"}:
|
|
1070
1364
|
raise TaskWorkflowError("TASK_PHASE_CLOSEOUT_REQUIRED", "Delivery Review and On-Site Closeout Phase must accept delivery_ready or no_go before complete")
|
|
1071
|
-
|
|
1072
|
-
if _artifact_digest(task_dir, closeout.get("artifact_refs", [])) != closeout.get("artifact_revision"):
|
|
1073
|
-
raise TaskWorkflowError("TASK_PHASE_STALE_ARTIFACT", "Delivery Review and On-Site Closeout Phase artifacts changed after acceptance; review the current delivery candidate again")
|
|
1365
|
+
_require_current_accepted_artifacts(task_dir, state)
|
|
1074
1366
|
readiness_error = task_closeout_readiness(data, disposition)
|
|
1075
1367
|
if readiness_error:
|
|
1076
1368
|
raise TaskWorkflowError("TASK_PHASE_CLOSEOUT_REQUIRED", readiness_error)
|
|
@@ -1081,7 +1373,8 @@ def cmd_complete_task(args: argparse.Namespace) -> int:
|
|
|
1081
1373
|
raise TaskWorkflowError("TASK_PHASE_IMPLEMENTATION_NOT_STARTED", "implementation tasks must enter in_progress before complete")
|
|
1082
1374
|
data["status"] = "completed"
|
|
1083
1375
|
data["completedAt"] = datetime.now().strftime("%Y-%m-%d")
|
|
1084
|
-
|
|
1376
|
+
from .task_move import relocate_task
|
|
1377
|
+
task_dir = relocate_task(get_repo_root(), task_dir, data)
|
|
1085
1378
|
from .active_task import clear_task_from_sessions
|
|
1086
1379
|
from .task_continuity import clear_task_runtime_records
|
|
1087
1380
|
|