@pennixrv/trellis 0.7.1-beta.16 → 0.7.1-beta.17

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.
@@ -0,0 +1,9 @@
1
+ {
2
+ "version": "0.7.1-beta.17",
3
+ "description": "区分合同内返工与合同变更,恢复终态任务的受控重开",
4
+ "breaking": false,
5
+ "recommendMigrate": false,
6
+ "changelog": "Execution rework preserves verified unchanged implementation authorization. New implementation contracts require a nonempty acceptance coverage table. Completed and cancelled tasks can explicitly reopen into planning, preserving terminal history and discarding stale runtime and authorization.",
7
+ "migrations": [],
8
+ "notes": "Run trellis update to refresh task scripts, the native workflow and phase Skills. Existing acceptance and authorization remain valid; new acceptance or authorization requires the coverage table. Reopen and phase return remain separate operations."
9
+ }
@@ -8,6 +8,8 @@
8
8
 
9
9
  合同结果已接受、实施尚未授权时,发现需要返修,应通过 `task.py phase return` 回到本阶段或更早受影响阶段,记录理由和证据。受影响的接受与授权随之失效;修订后重新接受当前产物,再核验实施授权。
10
10
 
11
+ 接受 `implementation_contract` 前,在合同中建立 `## 验收覆盖` 表,表头为 `接受条件 | 工作包 | Owner | 验证入口 | 交付证据 | 停止条件`。逐条映射已接受条件,核对实际源码 owner、可运行验证、证据落点和停止条件;单一工作包可以覆盖多条条件。CLI 仅检查表结构及非空内容,不能代替语义审查。目标、合同、决策与执行记录分别使用明确的文件引用,执行细节追加到 execution 或 research,避免修改已接受的上游合同。
12
+
11
13
  逐项决定回答后,将已决目标、候选依据、接口与数据流、owner、依赖顺序、实施和验证,以及适用的迁移、发布、资源和失败恢复安排组织为一个完整方案。核对需求覆盖、跨产物一致性、可实施性和可验收性;有净收益时使用有界反例或压力测试,先修复可查缺口。向用户展示可识别的整体方案和实质剩余取舍后,再记录当前 revision 的接受及实施授权。逐项答复不自动等于整体批准;用户已充分明确批准整体方案时不重复索取。目标或候选问题回最早受影响阶段,局部方案问题在本阶段解决,不增设外部封口状态。
12
14
 
13
15
  研究、原型和协作证据写回现有阶段产物、research 或 execution 自然落点,注明来源与版本、事实和假设、适用性、限制及决定影响。证据足够即停止扩搜;工具、报告或模板不能替代用户决定和 Trellis 原生接受事实。
@@ -28,4 +28,6 @@ flowchart TD
28
28
 
29
29
  ### 委派、回退与收敛
30
30
 
31
+ 合同不变的局部修复在当前活动阶段处理;需要撤销实施或交付接受事实时,使用 `phase return --return-kind execution_rework` 并提供原因、证据及目标阶段。仅在需求、合同摘要、路线和依赖仍有效时保留实施授权;CLI 拒绝时核对真实失效原因,不重复索取已有效的用户决定。合同实质变化使用默认 `contract_change` 回到最早受影响阶段并重新接受、授权。终态任务先经独立的 `reopen` 恢复到 `planning`,再操作工作流阶段;重开不复用旧授权。
32
+
31
33
  回退使用当前 owner 的 Git 和资源合同,不覆盖无关改动,也不删除仍需核验的现场。只有当前合同无法安全覆盖目标、授权、依赖、基线、接受条件或停止条件的变化时,才回退到最早受影响阶段。结束任务实施与测试验证阶段前,记录适用验证结果、未运行处置、合同差异和未结发现;仅当路线实际创建或委派资源时,记录其处置状态。
@@ -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
@@ -445,6 +446,31 @@ def _show(state: dict) -> int:
445
446
  return 0
446
447
 
447
448
 
449
+ def _require_acceptance_coverage(task_dir: Path, contract_ref: str) -> None:
450
+ _artifact_digest(task_dir, [contract_ref])
451
+ lines = (task_dir / contract_ref).read_text(encoding="utf-8").splitlines()
452
+ heading = next((index for index, line in enumerate(lines) if line.strip() == "## 验收覆盖"), None)
453
+ headers = ["接受条件", "工作包", "Owner", "验证入口", "交付证据", "停止条件"]
454
+ if heading is not None:
455
+ section = []
456
+ for line in lines[heading + 1:]:
457
+ if line.startswith("#"):
458
+ break
459
+ if line.strip():
460
+ section.append(line.strip())
461
+ rows = [line.strip("|").split("|") for line in section if line.startswith("|") and line.endswith("|")]
462
+ rows = [[cell.strip() for cell in row] for row in rows]
463
+ if (
464
+ len(rows) >= 3
465
+ and rows[0] == headers
466
+ and len(rows[1]) == len(headers)
467
+ and all(re.fullmatch(r":?-{3,}:?", cell) for cell in rows[1])
468
+ and all(len(row) == len(headers) and all(row) for row in rows[2:])
469
+ ):
470
+ return
471
+ raise TaskWorkflowError("TASK_PHASE_COVERAGE_REQUIRED", "implementation contract requires ## 验收覆盖 with nonempty rows: " + " | ".join(headers))
472
+
473
+
448
474
  def _update(args: argparse.Namespace, task_dir: Path, data: dict, state: dict) -> int:
449
475
  action = args.phase_action
450
476
  child_dispositions: dict[str, str] = {}
@@ -531,6 +557,8 @@ def _update(args: argparse.Namespace, task_dir: Path, data: dict, state: dict) -
531
557
  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")
532
558
  if result_ref not in record["artifact_refs"]:
533
559
  raise TaskWorkflowError("TASK_PHASE_RESULT_NOT_BOUND", "--result-ref must be included in the current phase artifact refs")
560
+ if current == PHASE_IDS[2] and outcome == "implementation_contract":
561
+ _require_acceptance_coverage(task_dir, result_ref)
534
562
  if current == PHASE_IDS[3]:
535
563
  authorization = state.get("implementation_authorization")
536
564
  if not isinstance(authorization, dict) or authorization.get("task_route_revision") != state["task_route_revision"]:
@@ -568,17 +596,38 @@ def _update(args: argparse.Namespace, task_dir: Path, data: dict, state: dict) -
568
596
  _advance(state, current)
569
597
  elif action == "return":
570
598
  target = _required_text(args.target_phase, "--target-phase")
599
+ return_kind = args.return_kind
600
+ execution_rework = return_kind == "execution_rework"
571
601
  if target not in state["phase_ids"] or (
572
602
  state["phase_ids"].index(target) >= state["phase_ids"].index(current)
573
- and not ((reviewed_delivery or accepted_design_return) and target == current)
603
+ and not ((reviewed_delivery or accepted_design_return or execution_rework) and target == current)
574
604
  ):
575
605
  raise TaskWorkflowError("TASK_PHASE_INVALID_TRANSITION", "return target must be an earlier phase in the current route")
576
606
  reason = _required_text(args.reason, "--reason")
577
607
  evidence = _refs(args.evidence_ref, "--evidence-ref", required=True)
608
+ authorization = state.get("implementation_authorization")
609
+ admission = state.get("route_dependency_admission")
610
+ if execution_rework:
611
+ if current not in PHASE_IDS[3:] or target not in PHASE_IDS[3:]:
612
+ raise TaskWorkflowError("TASK_PHASE_INVALID_TRANSITION", "execution_rework is limited to implementation and delivery phases")
613
+ candidate = copy.deepcopy(data)
614
+ candidate_state = candidate["meta"]["task_workflow"]
615
+ _invalidate_from(candidate_state, target, keep_first=True)
616
+ candidate_state["implementation_authorization"] = authorization
617
+ _require_current_accepted_artifacts(task_dir, candidate_state)
618
+ candidate_state["current_phase_id"] = PHASE_IDS[3]
619
+ candidate_state["phases"][PHASE_IDS[3]]["status"] = "active"
620
+ if not implementation_start_ready(candidate, task_dir):
621
+ raise TaskWorkflowError("TASK_PHASE_IMPLEMENTATION_NOT_AUTHORIZED", "execution_rework requires the unchanged authorized implementation contract")
622
+ if admission is not None and (admission.get("admission_state") != "ready" or admission.get("task_route_revision") != state["task_route_revision"]):
623
+ raise TaskWorkflowError("TASK_PHASE_ADMISSION_REQUIRED", "execution_rework requires current ready dependency admission")
578
624
  state["input_revision"] = _increment(state["input_revision"])
579
625
  _invalidate_from(state, target, keep_first=True)
626
+ if execution_rework:
627
+ state["implementation_authorization"] = authorization
628
+ state["route_dependency_admission"] = admission
580
629
  state["phases"][target]["input_revision"] = state["input_revision"]
581
- state["return_record"] = {"from": current, "to": target, "reason": reason, "evidence_refs": evidence, "task_route_revision": state["task_route_revision"]}
630
+ 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"]}
582
631
  elif action == "route":
583
632
  phases = _validate_route(args.phase_ids)
584
633
  task_kind = args.task_kind if args.task_kind is not None else state["task_kind"]
@@ -690,6 +739,7 @@ def _update(args: argparse.Namespace, task_dir: Path, data: dict, state: dict) -
690
739
  raise TaskWorkflowError("TASK_PHASE_STALE_ARTIFACT", "Solution Design and Convergence Phase implementation contract changed after acceptance; refresh and accept the current artifact before authorization")
691
740
  elif contract_ref not in state.get("skip_records", {}).get(PHASE_IDS[2], {}).get("evidence_refs", []):
692
741
  raise TaskWorkflowError("TASK_PHASE_CONTRACT_NOT_ACCEPTED", "when Solution Design and Convergence Phase is omitted, the implementation contract must be among its route evidence")
742
+ _require_acceptance_coverage(task_dir, contract_ref)
693
743
  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"]}
694
744
  if current == PHASE_IDS[2]:
695
745
  _advance(state, current)
@@ -1011,6 +1061,56 @@ def cmd_cancel_task(args: argparse.Namespace) -> int:
1011
1061
  return 1
1012
1062
 
1013
1063
 
1064
+ def cmd_reopen_task(args: argparse.Namespace) -> int:
1065
+ try:
1066
+ task_dir = _task_dir(args.task_dir)
1067
+ with _task_lock(task_dir):
1068
+ data, meta = _load(task_dir)
1069
+ previous_status = data.get("status")
1070
+ if previous_status not in {"completed", "cancelled"}:
1071
+ raise TaskWorkflowError("TASK_PHASE_REOPEN_INVALID_STATUS", "only completed or cancelled tasks can be reopened")
1072
+ reason = _required_text(args.reason, "--reason")
1073
+ evidence = _refs(args.evidence_ref, "--evidence-ref", required=True)
1074
+ _assert_delegated_resources_closed(task_dir, data)
1075
+ recoveries = meta.setdefault("task_lifecycle_recoveries", [])
1076
+ if not isinstance(recoveries, list) or any(not isinstance(record, dict) for record in recoveries):
1077
+ raise TaskWorkflowError("TASK_PHASE_TASK_INVALID", "task lifecycle recovery history is malformed")
1078
+ recovery = {
1079
+ "from_status": previous_status,
1080
+ "to_status": "planning",
1081
+ "reason": reason,
1082
+ "evidence_refs": evidence,
1083
+ "reopened_at": datetime.now(timezone.utc).isoformat(),
1084
+ "terminal_completed_at": data.get("completedAt"),
1085
+ "terminal_cancellation": copy.deepcopy(meta.get("task_cancellation")),
1086
+ }
1087
+ state = meta.get("task_workflow")
1088
+ if state is not None:
1089
+ state = _state(meta)
1090
+ state["implementation_authorization"] = None
1091
+ state["route_dependency_admission"] = None
1092
+ state["closeout_disposition"] = None
1093
+ recoveries.append(recovery)
1094
+ data["status"] = "planning"
1095
+ data["completedAt"] = None
1096
+ from .task_move import relocate_task
1097
+ from .active_task import clear_task_from_sessions
1098
+ from .task_continuity import clear_task_runtime_records
1099
+
1100
+ repo_root = get_repo_root()
1101
+ # Relocation rewrites runtime paths; terminal runtime must be discarded first.
1102
+ for runtime_dir in (task_dir, repo_root / ".trellis/tasks/planning" / task_dir.name):
1103
+ clear_task_from_sessions(str(runtime_dir), repo_root)
1104
+ if not clear_task_runtime_records(repo_root, runtime_dir.relative_to(repo_root).as_posix()):
1105
+ raise TaskWorkflowError("TASK_PHASE_RUNTIME_CLEANUP_FAILED", "could not discard terminal task runtime; task remains terminal")
1106
+ task_dir = relocate_task(repo_root, task_dir, data)
1107
+ print(json.dumps({"status": "planning", "task_lifecycle_recovery": recovery, "runtime_records_cleared": True}, ensure_ascii=False))
1108
+ return 0
1109
+ except TaskWorkflowError as error:
1110
+ print(f"{error.code}: {error}", file=sys.stderr)
1111
+ return 1
1112
+
1113
+
1014
1114
  def task_closeout_readiness(data: dict, disposition: str) -> str | None:
1015
1115
  if "meta" in data and not isinstance(data["meta"], dict):
1016
1116
  return "task metadata is malformed"
@@ -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,6 +80,7 @@ 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,
84
86
  cmd_phase,
@@ -779,6 +781,7 @@ def main() -> int:
779
781
  p_phase.add_argument("--route-reason", help="Reason when initialization omits one or more phases")
780
782
  p_phase.add_argument("--route-evidence-ref", action="append", help="Evidence supporting an abbreviated initial route")
781
783
  p_phase.add_argument("--target-phase", help="Earlier phase to revisit, or first affected phase for route change")
784
+ 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
785
  p_phase.add_argument("--artifact-ref", action="append", help="Task-relative file in the current phase output")
783
786
  p_phase.add_argument("--artifact-revision", help="Current artifact_revision required for acceptance")
784
787
  p_phase.add_argument("--result-ref", help="Task result artifact reference")
@@ -803,6 +806,11 @@ def main() -> int:
803
806
  p_cancel.add_argument("--reason", required=True, help="Why the task is cancelled")
804
807
  p_cancel.add_argument("--evidence-ref", action="append", required=True, help="Task-relative cancellation evidence")
805
808
 
809
+ p_reopen = subparsers.add_parser("reopen", help="Recover a terminal task to planning without implementation authorization")
810
+ p_reopen.add_argument("task_dir", help="Task directory or task name")
811
+ p_reopen.add_argument("--reason", required=True, help="Why the same task must continue")
812
+ p_reopen.add_argument("--evidence-ref", action="append", required=True, help="Recovery decision evidence")
813
+
806
814
  # set-branch
807
815
  p_branch = subparsers.add_parser("set-branch", help="Set git branch")
808
816
  p_branch.add_argument("dir", help="Task directory")
@@ -880,6 +888,7 @@ def main() -> int:
880
888
  "delegate-start": cmd_delegate_start,
881
889
  "complete": cmd_complete_task,
882
890
  "cancel": cmd_cancel_task,
891
+ "reopen": cmd_reopen_task,
883
892
  "set-branch": cmd_set_branch,
884
893
  "set-base-branch": cmd_set_base_branch,
885
894
  "set-scope": cmd_set_scope,
@@ -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 会读取清单时才需要维护;清单不能替代任务资产或阶段证据。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pennixrv/trellis",
3
- "version": "0.7.1-beta.16",
3
+ "version": "0.7.1-beta.17",
4
4
  "description": "AI capabilities grow like ivy — Trellis provides the structure to guide them along a disciplined path",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -35,7 +35,7 @@
35
35
  "inquirer": "^9.3.7",
36
36
  "undici": "^6.21.0",
37
37
  "zod": "^4.4.2",
38
- "@pennixrv/trellis-core": "0.7.1-beta.16"
38
+ "@pennixrv/trellis-core": "0.7.1-beta.17"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@eslint/js": "^9.18.0",