@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.
- package/dist/cli/index.js +4 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +39 -26
- package/dist/commands/init.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 +41 -29
- 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 -3
- package/dist/commands/teamwork/teammate-adapters.d.ts.map +1 -1
- package/dist/commands/teamwork/teammate-adapters.js +9 -54
- 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 +14 -4
- package/dist/commands/teamwork/teammate-launch.d.ts.map +1 -1
- package/dist/commands/teamwork/teammate-launch.js +129 -20
- 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/update.d.ts.map +1 -1
- package/dist/commands/update.js +4 -3
- package/dist/commands/update.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/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.6.json +1 -1
- package/dist/migrations/manifests/0.7.2-beta.0.json +11 -0
- 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-meta/references/local-architecture/multi-agent-teamwork.md +14 -2
- package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/task-system.md +1 -1
- package/dist/templates/common/bundled-skills/trellis-solution-design-and-convergence/workflow.md +4 -0
- package/dist/templates/common/bundled-skills/trellis-task-implementation/workflow.md +6 -0
- package/dist/templates/common/bundled-skills/trellis-teamwork/SKILL.md +14 -4
- package/dist/templates/common/bundled-skills/trellis-teamwork/references/coordination-modes.md +31 -2
- package/dist/templates/common/bundled-skills/trellis-teamwork/references/teammate-configuration-matrix.md +37 -31
- package/dist/templates/common/bundled-skills/trellis-teamwork/references/teammate-processes.md +94 -24
- package/dist/templates/common/bundled-skills/trellis-teamwork/references/teamwork-plan-construction.md +112 -20
- 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/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 +1 -1
- 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/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 -63
- package/dist/templates/trellis/config.yaml +2 -5
- package/dist/templates/trellis/index.d.ts +1 -0
- package/dist/templates/trellis/index.d.ts.map +1 -1
- package/dist/templates/trellis/index.js +16 -4
- package/dist/templates/trellis/index.js.map +1 -1
- package/dist/templates/trellis/scripts/common/task_continuity.py +54 -28
- package/dist/templates/trellis/scripts/common/task_store.py +7 -2
- package/dist/templates/trellis/scripts/common/task_workflow.py +270 -18
- package/dist/templates/trellis/scripts/task.py +27 -3
- package/dist/templates/trellis/teamwork.json +10 -0
- package/dist/templates/trellis/workflow.md +7 -0
- package/dist/utils/asset-backup.d.ts.map +1 -1
- package/dist/utils/asset-backup.js +10 -4
- package/dist/utils/asset-backup.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/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
|
|
@@ -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
|
|
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
|
-
|
|
241
|
+
return (
|
|
239
242
|
state["phase_ids"] == [PHASE_IDS[0], PHASE_IDS[4]]
|
|
240
|
-
and state.get("current_phase_id")
|
|
243
|
+
and state.get("current_phase_id") in {PHASE_IDS[4], None}
|
|
241
244
|
and isinstance(requirements, dict)
|
|
242
|
-
and
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AA6BxB,QAAA,MAAM,iBAAiB;;;;;;;oBAGrB,CAAC;AASH,QAAA,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAkCT,CAAC;AACZ,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AACzD,KAAK,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"asset-backup.d.ts","sourceRoot":"","sources":["../../src/utils/asset-backup.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AA6BxB,QAAA,MAAM,iBAAiB;;;;;;;oBAGrB,CAAC;AASH,QAAA,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAkCT,CAAC;AACZ,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AACzD,KAAK,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAiFrD,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,WAAW,CAmCrE;AAED,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,EACZ,KAAK,GAAE,QAAQ,CAAC,MAAM,CAAM,GAC3B,MAAM,EAAE,CAIV;AAED,oFAAoF;AACpF,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,MAAM,GAAE,WAAW,CAAC,QAAQ,CAAY,EACxC,KAAK,WAAyB,GAC7B,WAAW,CA2Db;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAa9D;AAED,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,UAAU,GAAG,QAAQ,GAAG,SAAS,GAAG,WAAW,CAAC;IACtD,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAiBD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,mBAAmB,EAAE,CAuGpE;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAQxD;AAED,wBAAgB,oBAAoB,CAAC,CAAC,EACpC,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,CAAC,EAClB,QAAQ,UAAQ,GACf,CAAC,CAUH;AAED,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE;IACP,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACV,GACL;IACD,QAAQ,EAAE,mBAAmB,EAAE,CAAC;IAChC,SAAS,EAAE,mBAAmB,EAAE,CAAC;IACjC,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAC3C,CA8CA;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE;QACP,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,OAAO,GAAG,QAAQ,GAAG,MAAM,GAAG,UAAU,CAAC;QACjD,OAAO,EAAE,WAAW,CAAC;QACrB,OAAO,EAAE,WAAW,CAAC;KACtB,EAAE,CAAC;CACL;AAsBD,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,EACV,eAAe,UAAQ,GACtB,gBAAgB,CAsDlB;AAED,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,EACV,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,eAAe,CAAC,EAAE,OAAO,CAAC;IAAC,GAAG,CAAC,EAAE,OAAO,CAAA;CAAO,GAC1E,gBAAgB,CAmDlB"}
|