trellis-hgl 0.6.0-beta.28 → 0.6.0-beta.29
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/migrations/manifests/0.6.0-beta.29.json +9 -0
- package/dist/templates/shared-hooks/inject-subagent-context.py +108 -13
- package/dist/templates/shared-hooks/session-start.py +31 -212
- 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 +2 -0
- package/dist/templates/trellis/index.js.map +1 -1
- package/dist/templates/trellis/scripts/common/worktree_sync.py +273 -0
- package/package.json +2 -2
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "0.6.0-beta.29",
|
|
3
|
+
"description": "Beta 补丁:补齐 Claude shared worktree 的 .trellis/scripts bootstrap,并修正 worktree 漂移检测与覆盖保护。",
|
|
4
|
+
"breaking": false,
|
|
5
|
+
"recommendMigrate": false,
|
|
6
|
+
"changelog": "**修复:**\n- fix(worktree): Claude shared worktree 与 Trellis-managed worktree 在派发前会同步 `.trellis/scripts/`、`.trellis/workflow.md`、`.trellis/config.yaml` 与 `.trellis/.gitignore`,避免 subagent 在 worktree 内执行 `./.trellis/scripts/*` 时因缺少脚本而报错。\n- fix(hooks): `inject-subagent-context.py`、`session-start.py` 与 `common/worktree_sync.py` 现在共享同一套 bootstrap、planning snapshot 同步与 drift 检测逻辑,模板与 dogfood 副本保持一致。\n- fix(worktree): \"local code changes\" 现在基于 `git status --porcelain -z --untracked-files=all` 判断,已填充但 git-clean 的 worktree 不再被误判为 dirty;真正有代码改动时仍会阻止主工作区自动覆盖。\n\n**说明:**\n- 现有项目执行 `trellis update` 后即可获得新的 shared worktree scripts/bootstrap 与漂移保护修复。",
|
|
7
|
+
"migrations": [],
|
|
8
|
+
"notes": "安装此版本后请运行 `trellis update`,以同步生成的 shared worktree hooks、`.trellis/scripts/` 与 planning drift 保护逻辑;除非你本地手改过生成模板,否则无需手动迁移。"
|
|
9
|
+
}
|
|
@@ -49,8 +49,6 @@ if sys.platform.startswith("win"):
|
|
|
49
49
|
DIR_WORKFLOW = ".trellis"
|
|
50
50
|
DIR_SPEC = "spec"
|
|
51
51
|
FILE_TASK_JSON = "task.json"
|
|
52
|
-
WORKTREE_PARENT_DIR = ".trellis"
|
|
53
|
-
WORKTREE_ROOT_DIR = "trellis-worktrees"
|
|
54
52
|
|
|
55
53
|
# =============================================================================
|
|
56
54
|
# Subagent Constants (change here to rename subagent types)
|
|
@@ -95,21 +93,64 @@ def find_repo_root(start_path: str) -> str | None:
|
|
|
95
93
|
return None
|
|
96
94
|
|
|
97
95
|
|
|
96
|
+
def _candidate_scripts_dirs(repo_root: Path) -> list[Path]:
|
|
97
|
+
candidates = [repo_root / DIR_WORKFLOW / "scripts"]
|
|
98
|
+
try:
|
|
99
|
+
if repo_root.parent.name == "trellis-worktrees" and repo_root.parent.parent.name == DIR_WORKFLOW:
|
|
100
|
+
candidates.insert(0, repo_root.parent.parent.parent / DIR_WORKFLOW / "scripts")
|
|
101
|
+
except Exception:
|
|
102
|
+
pass
|
|
103
|
+
return candidates
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _load_worktree_sync(repo_root: str):
|
|
107
|
+
repo_path = Path(repo_root).resolve()
|
|
108
|
+
for scripts_dir in _candidate_scripts_dirs(repo_path):
|
|
109
|
+
if str(scripts_dir) not in sys.path:
|
|
110
|
+
sys.path.insert(0, str(scripts_dir))
|
|
111
|
+
try:
|
|
112
|
+
from common import worktree_sync # type: ignore[import-not-found]
|
|
113
|
+
return worktree_sync
|
|
114
|
+
except Exception:
|
|
115
|
+
continue
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
|
|
98
119
|
def _infer_worktree_task(repo_root: str) -> str | None:
|
|
99
|
-
|
|
120
|
+
worktree_sync = _load_worktree_sync(repo_root)
|
|
121
|
+
if worktree_sync is None:
|
|
122
|
+
return None
|
|
100
123
|
try:
|
|
101
|
-
|
|
102
|
-
return None
|
|
103
|
-
if root.parent.parent.name != WORKTREE_PARENT_DIR:
|
|
104
|
-
return None
|
|
124
|
+
return worktree_sync.infer_managed_worktree_task(Path(repo_root).resolve())
|
|
105
125
|
except Exception:
|
|
106
126
|
return None
|
|
107
127
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if not task_dir
|
|
111
|
-
return
|
|
112
|
-
|
|
128
|
+
|
|
129
|
+
def ensure_shared_worktree_bootstrap(repo_root: str, task_dir: str | None) -> None:
|
|
130
|
+
if not task_dir:
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
worktree_sync = _load_worktree_sync(repo_root)
|
|
134
|
+
if worktree_sync is None:
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
task_dir_name = Path(task_dir).name
|
|
138
|
+
if not task_dir_name:
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
resolved = worktree_sync.resolve_shared_worktree_roots(
|
|
142
|
+
Path(repo_root).resolve(),
|
|
143
|
+
task_dir_name,
|
|
144
|
+
)
|
|
145
|
+
if not resolved:
|
|
146
|
+
return
|
|
147
|
+
|
|
148
|
+
main_root, worktree_root = resolved
|
|
149
|
+
worktree_sync.sync_runtime_bundle(main_root, worktree_root)
|
|
150
|
+
|
|
151
|
+
target_task_dir = worktree_sync.task_dir(worktree_root, task_dir_name)
|
|
152
|
+
if not worktree_sync.has_any_task_artifact(target_task_dir):
|
|
153
|
+
worktree_sync.sync_task_snapshot(main_root, worktree_root, task_dir_name)
|
|
113
154
|
|
|
114
155
|
|
|
115
156
|
def _detect_platform(input_data: dict) -> str | None:
|
|
@@ -880,6 +921,15 @@ def _looks_like_path_key(key: str | None) -> bool:
|
|
|
880
921
|
return lowered.endswith(("_path", "_paths", "_file", "_files", "_dir", "_root"))
|
|
881
922
|
|
|
882
923
|
|
|
924
|
+
def _extract_shared_worktree_task_name(path_value: Any) -> str | None:
|
|
925
|
+
normalized = _normalize_hook_text(path_value)
|
|
926
|
+
if CLAUDE_SHARED_WORKTREE_MARKER not in normalized:
|
|
927
|
+
return None
|
|
928
|
+
suffix = normalized.split(CLAUDE_SHARED_WORKTREE_MARKER, 1)[1]
|
|
929
|
+
task_name = suffix.split("/", 1)[0].strip()
|
|
930
|
+
return task_name or None
|
|
931
|
+
|
|
932
|
+
|
|
883
933
|
def _tool_input_targets_shared_worktree(value: Any, key: str | None = None) -> bool:
|
|
884
934
|
if isinstance(value, dict):
|
|
885
935
|
for nested_key, nested_value in value.items():
|
|
@@ -896,6 +946,40 @@ def _tool_input_targets_shared_worktree(value: Any, key: str | None = None) -> b
|
|
|
896
946
|
return _path_uses_shared_worktree(value)
|
|
897
947
|
|
|
898
948
|
|
|
949
|
+
def _tool_input_shared_worktree_task(value: Any, key: str | None = None) -> str | None:
|
|
950
|
+
if isinstance(value, dict):
|
|
951
|
+
for nested_key, nested_value in value.items():
|
|
952
|
+
task_name = _tool_input_shared_worktree_task(nested_value, str(nested_key))
|
|
953
|
+
if task_name:
|
|
954
|
+
return task_name
|
|
955
|
+
return None
|
|
956
|
+
if isinstance(value, list):
|
|
957
|
+
for item in value:
|
|
958
|
+
task_name = _tool_input_shared_worktree_task(item, key)
|
|
959
|
+
if task_name:
|
|
960
|
+
return task_name
|
|
961
|
+
return None
|
|
962
|
+
if not _looks_like_path_key(key):
|
|
963
|
+
return None
|
|
964
|
+
return _extract_shared_worktree_task_name(value)
|
|
965
|
+
|
|
966
|
+
|
|
967
|
+
def infer_task_dir_from_shared_worktree_signal(
|
|
968
|
+
repo_root: str,
|
|
969
|
+
tool_input: dict,
|
|
970
|
+
cwd: str,
|
|
971
|
+
) -> str | None:
|
|
972
|
+
for path_value in (cwd, repo_root):
|
|
973
|
+
task_name = _extract_shared_worktree_task_name(path_value)
|
|
974
|
+
if task_name:
|
|
975
|
+
return f".trellis/tasks/{task_name}"
|
|
976
|
+
|
|
977
|
+
task_name = _tool_input_shared_worktree_task(tool_input)
|
|
978
|
+
if task_name:
|
|
979
|
+
return f".trellis/tasks/{task_name}"
|
|
980
|
+
return None
|
|
981
|
+
|
|
982
|
+
|
|
899
983
|
def has_shared_worktree_signal(
|
|
900
984
|
repo_root: str,
|
|
901
985
|
task_dir: str | None,
|
|
@@ -964,6 +1048,8 @@ def main():
|
|
|
964
1048
|
|
|
965
1049
|
# Get current task directory (research doesn't require it)
|
|
966
1050
|
task_dir = get_current_task(repo_root, input_data)
|
|
1051
|
+
if not task_dir and is_claude_code_dev_agent(subagent_type):
|
|
1052
|
+
task_dir = infer_task_dir_from_shared_worktree_signal(repo_root, tool_input, cwd)
|
|
967
1053
|
|
|
968
1054
|
# implement/check/review need task directory
|
|
969
1055
|
if subagent_type in AGENTS_REQUIRE_TASK:
|
|
@@ -977,8 +1063,17 @@ def main():
|
|
|
977
1063
|
hook_notice = ""
|
|
978
1064
|
hook_system_message = ""
|
|
979
1065
|
normalized_tool_input = tool_input
|
|
1066
|
+
shared_worktree_signal = False
|
|
1067
|
+
|
|
980
1068
|
if platform == "claude" and is_claude_code_dev_agent(subagent_type):
|
|
981
|
-
|
|
1069
|
+
shared_worktree_signal = has_shared_worktree_signal(
|
|
1070
|
+
repo_root,
|
|
1071
|
+
task_dir,
|
|
1072
|
+
tool_input,
|
|
1073
|
+
cwd,
|
|
1074
|
+
)
|
|
1075
|
+
if shared_worktree_signal:
|
|
1076
|
+
ensure_shared_worktree_bootstrap(repo_root, task_dir)
|
|
982
1077
|
normalized_tool_input, stripped = strip_conflicting_worktree_isolation(tool_input)
|
|
983
1078
|
if stripped:
|
|
984
1079
|
hook_notice = _build_shared_worktree_conflict_notice(task_dir)
|
|
@@ -9,12 +9,10 @@ from __future__ import annotations
|
|
|
9
9
|
import warnings
|
|
10
10
|
warnings.filterwarnings("ignore")
|
|
11
11
|
|
|
12
|
-
import hashlib
|
|
13
12
|
import json
|
|
14
13
|
import os
|
|
15
14
|
import re
|
|
16
15
|
import shlex
|
|
17
|
-
import shutil
|
|
18
16
|
import subprocess
|
|
19
17
|
import sys
|
|
20
18
|
from io import StringIO
|
|
@@ -99,210 +97,28 @@ if sys.platform.startswith("win"):
|
|
|
99
97
|
|
|
100
98
|
WORKTREE_PARENT_DIR = ".trellis"
|
|
101
99
|
WORKTREE_ROOT_DIR = "trellis-worktrees"
|
|
102
|
-
RUNTIME_BUNDLE_FILES = (
|
|
103
|
-
".trellis/workflow.md",
|
|
104
|
-
".trellis/config.yaml",
|
|
105
|
-
".trellis/.gitignore",
|
|
106
|
-
)
|
|
107
|
-
RUNTIME_BUNDLE_DIRS = (
|
|
108
|
-
".trellis/scripts",
|
|
109
|
-
)
|
|
110
|
-
PLANNING_FILE_NAMES = (
|
|
111
|
-
"task.json",
|
|
112
|
-
"prd.md",
|
|
113
|
-
"design.md",
|
|
114
|
-
"implement.md",
|
|
115
|
-
"implement.jsonl",
|
|
116
|
-
"check.jsonl",
|
|
117
|
-
)
|
|
118
|
-
PLANNING_DIR_NAMES = ("research",)
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
def _remove_path(path: Path) -> None:
|
|
122
|
-
if path.is_symlink() or path.is_file():
|
|
123
|
-
path.unlink()
|
|
124
|
-
return
|
|
125
|
-
if path.is_dir():
|
|
126
|
-
shutil.rmtree(path)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
def _hash_file(path: Path) -> str:
|
|
130
|
-
digest = hashlib.sha256()
|
|
131
|
-
with path.open("rb") as handle:
|
|
132
|
-
for chunk in iter(lambda: handle.read(65536), b""):
|
|
133
|
-
digest.update(chunk)
|
|
134
|
-
return digest.hexdigest()
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
def _snapshot_dir(path: Path) -> dict[str, str]:
|
|
138
|
-
if not path.is_dir():
|
|
139
|
-
return {}
|
|
140
|
-
snapshot: dict[str, str] = {}
|
|
141
|
-
for child in sorted(path.rglob("*")):
|
|
142
|
-
if child.is_file():
|
|
143
|
-
snapshot[child.relative_to(path).as_posix()] = _hash_file(child)
|
|
144
|
-
return snapshot
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
def _same_file(src: Path, dst: Path) -> bool:
|
|
148
|
-
return src.is_file() and dst.is_file() and _hash_file(src) == _hash_file(dst)
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
def _same_tree(src: Path, dst: Path) -> bool:
|
|
152
|
-
return src.is_dir() and dst.is_dir() and _snapshot_dir(src) == _snapshot_dir(dst)
|
|
153
|
-
|
|
154
100
|
|
|
155
|
-
def _copy_file(src: Path, dst: Path) -> None:
|
|
156
|
-
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
157
|
-
shutil.copy2(src, dst)
|
|
158
101
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
if dst.exists():
|
|
162
|
-
_remove_path(dst)
|
|
163
|
-
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
164
|
-
shutil.copytree(src, dst)
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
def _detect_trellis_managed_worktree(repo_root: Path) -> tuple[Path, str] | None:
|
|
102
|
+
def _candidate_scripts_dirs(repo_root: Path) -> list[Path]:
|
|
103
|
+
candidates = [repo_root / ".trellis" / "scripts"]
|
|
168
104
|
try:
|
|
169
|
-
if repo_root.parent.name
|
|
170
|
-
|
|
171
|
-
if repo_root.parent.parent.name != WORKTREE_PARENT_DIR:
|
|
172
|
-
return None
|
|
173
|
-
main_root = repo_root.parent.parent.parent
|
|
174
|
-
if not (main_root / ".git").exists():
|
|
175
|
-
return None
|
|
176
|
-
return main_root, repo_root.name
|
|
105
|
+
if repo_root.parent.name == WORKTREE_ROOT_DIR and repo_root.parent.parent.name == WORKTREE_PARENT_DIR:
|
|
106
|
+
candidates.insert(0, repo_root.parent.parent.parent / ".trellis" / "scripts")
|
|
177
107
|
except Exception:
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
def _task_dir(repo_root: Path, task_dir_name: str) -> Path:
|
|
182
|
-
return repo_root / ".trellis" / "tasks" / task_dir_name
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
def _task_snapshot(task_dir: Path) -> dict[str, str]:
|
|
186
|
-
snapshot: dict[str, str] = {}
|
|
187
|
-
for name in PLANNING_FILE_NAMES:
|
|
188
|
-
file_path = task_dir / name
|
|
189
|
-
if file_path.is_file():
|
|
190
|
-
snapshot[name] = _hash_file(file_path)
|
|
191
|
-
for name in PLANNING_DIR_NAMES:
|
|
192
|
-
dir_path = task_dir / name
|
|
193
|
-
if not dir_path.is_dir():
|
|
194
|
-
continue
|
|
195
|
-
for child in sorted(dir_path.rglob("*")):
|
|
196
|
-
if child.is_file():
|
|
197
|
-
snapshot[child.relative_to(task_dir).as_posix()] = _hash_file(child)
|
|
198
|
-
return snapshot
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
def _has_any_task_artifact(task_dir: Path) -> bool:
|
|
202
|
-
for name in PLANNING_FILE_NAMES:
|
|
203
|
-
if (task_dir / name).is_file():
|
|
204
|
-
return True
|
|
205
|
-
for name in PLANNING_DIR_NAMES:
|
|
206
|
-
if (task_dir / name).exists():
|
|
207
|
-
return True
|
|
208
|
-
return False
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
def _sync_runtime_bundle(main_root: Path, worktree_root: Path) -> list[str]:
|
|
212
|
-
synced: list[str] = []
|
|
213
|
-
for relative_path in RUNTIME_BUNDLE_FILES:
|
|
214
|
-
src = main_root / relative_path
|
|
215
|
-
dst = worktree_root / relative_path
|
|
216
|
-
if not src.is_file() or _same_file(src, dst):
|
|
217
|
-
continue
|
|
218
|
-
_copy_file(src, dst)
|
|
219
|
-
synced.append(relative_path)
|
|
220
|
-
for relative_path in RUNTIME_BUNDLE_DIRS:
|
|
221
|
-
src = main_root / relative_path
|
|
222
|
-
dst = worktree_root / relative_path
|
|
223
|
-
if not src.is_dir() or _same_tree(src, dst):
|
|
224
|
-
continue
|
|
225
|
-
_copy_tree(src, dst)
|
|
226
|
-
synced.append(relative_path)
|
|
227
|
-
return synced
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
def _sync_task_snapshot(main_root: Path, worktree_root: Path, task_dir_name: str) -> list[str]:
|
|
231
|
-
source_task_dir = _task_dir(main_root, task_dir_name)
|
|
232
|
-
target_task_dir = _task_dir(worktree_root, task_dir_name)
|
|
233
|
-
if not source_task_dir.is_dir():
|
|
234
|
-
return []
|
|
235
|
-
|
|
236
|
-
synced: list[str] = []
|
|
237
|
-
for name in PLANNING_FILE_NAMES:
|
|
238
|
-
src = source_task_dir / name
|
|
239
|
-
if not src.is_file():
|
|
240
|
-
continue
|
|
241
|
-
dst = target_task_dir / name
|
|
242
|
-
if _same_file(src, dst):
|
|
243
|
-
continue
|
|
244
|
-
_copy_file(src, dst)
|
|
245
|
-
synced.append(name)
|
|
246
|
-
|
|
247
|
-
for name in PLANNING_DIR_NAMES:
|
|
248
|
-
src = source_task_dir / name
|
|
249
|
-
if not src.is_dir():
|
|
250
|
-
continue
|
|
251
|
-
dst = target_task_dir / name
|
|
252
|
-
if _same_tree(src, dst):
|
|
253
|
-
continue
|
|
254
|
-
_copy_tree(src, dst)
|
|
255
|
-
synced.append(name)
|
|
256
|
-
|
|
257
|
-
return synced
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
def _collect_task_drift(main_root: Path, worktree_root: Path, task_dir_name: str) -> list[str]:
|
|
261
|
-
source_snapshot = _task_snapshot(_task_dir(main_root, task_dir_name))
|
|
262
|
-
target_snapshot = _task_snapshot(_task_dir(worktree_root, task_dir_name))
|
|
263
|
-
keys = sorted(set(source_snapshot) | set(target_snapshot))
|
|
264
|
-
return [key for key in keys if source_snapshot.get(key) != target_snapshot.get(key)]
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
def _is_managed_worktree_path(path_str: str, task_dir_name: str) -> bool:
|
|
268
|
-
normalized = path_str.replace("\\", "/").strip().strip("/")
|
|
269
|
-
|
|
270
|
-
for relative_path in RUNTIME_BUNDLE_FILES:
|
|
271
|
-
runtime_path = relative_path.strip("/")
|
|
272
|
-
if normalized == runtime_path:
|
|
273
|
-
return True
|
|
274
|
-
for relative_path in RUNTIME_BUNDLE_DIRS:
|
|
275
|
-
runtime_dir = relative_path.strip("/")
|
|
276
|
-
if normalized == runtime_dir or normalized.startswith(runtime_dir + "/"):
|
|
277
|
-
return True
|
|
278
|
-
|
|
279
|
-
task_root = f".trellis/tasks/{task_dir_name}"
|
|
280
|
-
if normalized in (".trellis/tasks", task_root):
|
|
281
|
-
return True
|
|
282
|
-
task_prefix = task_root + "/"
|
|
283
|
-
if not normalized.startswith(task_prefix):
|
|
284
|
-
return False
|
|
285
|
-
task_relative = normalized[len(task_prefix):]
|
|
286
|
-
if not task_relative:
|
|
287
|
-
return True
|
|
288
|
-
if task_relative in PLANNING_FILE_NAMES:
|
|
289
|
-
return True
|
|
290
|
-
for dir_name in PLANNING_DIR_NAMES:
|
|
291
|
-
if task_relative == dir_name or task_relative.startswith(dir_name + "/"):
|
|
292
|
-
return True
|
|
293
|
-
return False
|
|
108
|
+
pass
|
|
109
|
+
return candidates
|
|
294
110
|
|
|
295
111
|
|
|
296
|
-
def
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
112
|
+
def _load_worktree_sync(repo_root: Path):
|
|
113
|
+
for scripts_dir in _candidate_scripts_dirs(repo_root):
|
|
114
|
+
if str(scripts_dir) not in sys.path:
|
|
115
|
+
sys.path.insert(0, str(scripts_dir))
|
|
116
|
+
try:
|
|
117
|
+
from common import worktree_sync # type: ignore[import-not-found]
|
|
118
|
+
return worktree_sync
|
|
119
|
+
except Exception:
|
|
301
120
|
continue
|
|
302
|
-
|
|
303
|
-
if not _is_managed_worktree_path(relative_path, task_dir_name):
|
|
304
|
-
return True
|
|
305
|
-
return False
|
|
121
|
+
return None
|
|
306
122
|
|
|
307
123
|
|
|
308
124
|
def _format_changed_paths(paths: list[str], limit: int = 6) -> str:
|
|
@@ -313,7 +129,11 @@ def _format_changed_paths(paths: list[str], limit: int = 6) -> str:
|
|
|
313
129
|
|
|
314
130
|
def _maybe_sync_trellis_worktree(project_dir: Path) -> str:
|
|
315
131
|
try:
|
|
316
|
-
|
|
132
|
+
worktree_sync = _load_worktree_sync(project_dir)
|
|
133
|
+
if worktree_sync is None:
|
|
134
|
+
return ""
|
|
135
|
+
|
|
136
|
+
detected = worktree_sync.detect_trellis_managed_worktree(project_dir)
|
|
317
137
|
if not detected:
|
|
318
138
|
return ""
|
|
319
139
|
|
|
@@ -322,29 +142,29 @@ def _maybe_sync_trellis_worktree(project_dir: Path) -> str:
|
|
|
322
142
|
f"Detected Trellis-managed worktree: ./.trellis/trellis-worktrees/{task_dir_name}/",
|
|
323
143
|
]
|
|
324
144
|
|
|
325
|
-
runtime_synced =
|
|
145
|
+
runtime_synced = worktree_sync.sync_runtime_bundle(main_root, project_dir)
|
|
326
146
|
if runtime_synced:
|
|
327
147
|
notes.append(
|
|
328
148
|
"Bootstrapped runtime bundle from main workspace: "
|
|
329
149
|
+ ", ".join(runtime_synced)
|
|
330
150
|
)
|
|
331
151
|
|
|
332
|
-
target_task_dir =
|
|
333
|
-
if not
|
|
334
|
-
planning_synced =
|
|
152
|
+
target_task_dir = worktree_sync.task_dir(project_dir, task_dir_name)
|
|
153
|
+
if not worktree_sync.has_any_task_artifact(target_task_dir):
|
|
154
|
+
planning_synced = worktree_sync.sync_task_snapshot(main_root, project_dir, task_dir_name)
|
|
335
155
|
if planning_synced:
|
|
336
156
|
notes.append(
|
|
337
157
|
"Bootstrapped current task planning snapshot from main workspace: "
|
|
338
158
|
+ ", ".join(planning_synced)
|
|
339
159
|
)
|
|
340
160
|
|
|
341
|
-
drift =
|
|
161
|
+
drift = worktree_sync.collect_task_drift(main_root, project_dir, task_dir_name)
|
|
342
162
|
if drift:
|
|
343
163
|
notes.append(
|
|
344
164
|
"Planning drift detected between main workspace and worktree: "
|
|
345
165
|
+ _format_changed_paths(drift)
|
|
346
166
|
)
|
|
347
|
-
if
|
|
167
|
+
if worktree_sync.worktree_has_local_code_changes(project_dir, task_dir_name):
|
|
348
168
|
notes.append(
|
|
349
169
|
"Do NOT auto-overwrite: this worktree has local code changes. "
|
|
350
170
|
"Explain the conflict first, recommend creating a new Trellis task "
|
|
@@ -594,14 +414,13 @@ def run_script(script_path: Path, context_key: str | None = None) -> str:
|
|
|
594
414
|
|
|
595
415
|
|
|
596
416
|
def _infer_worktree_task_ref(repo_root: Path) -> str | None:
|
|
597
|
-
|
|
598
|
-
if
|
|
417
|
+
worktree_sync = _load_worktree_sync(repo_root)
|
|
418
|
+
if worktree_sync is None:
|
|
599
419
|
return None
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
420
|
+
try:
|
|
421
|
+
return worktree_sync.infer_managed_worktree_task(repo_root)
|
|
422
|
+
except Exception:
|
|
603
423
|
return None
|
|
604
|
-
return f".trellis/tasks/{task_dir_name}"
|
|
605
424
|
|
|
606
425
|
|
|
607
426
|
def _normalize_task_ref(task_ref: str) -> str:
|
|
@@ -37,6 +37,7 @@ export declare const commonPackagesContext: string;
|
|
|
37
37
|
export declare const commonWorkflowPhase: string;
|
|
38
38
|
export declare const commonTrellisConfig: string;
|
|
39
39
|
export declare const commonSafeCommit: string;
|
|
40
|
+
export declare const commonWorktreeSync: string;
|
|
40
41
|
export declare const trellisSwitch: string;
|
|
41
42
|
export declare const assertTrellisEnabled: string;
|
|
42
43
|
export declare const getDeveloperScript: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/templates/trellis/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAcH,eAAO,MAAM,WAAW,QAAsC,CAAC;AAG/D,eAAO,MAAM,UAAU,QAA6C,CAAC;AACrE,eAAO,MAAM,WAAW,QAA0C,CAAC;AACnE,eAAO,MAAM,eAAe,QAA8C,CAAC;AAC3E,eAAO,MAAM,gBAAgB,QAAgD,CAAC;AAC9E,eAAO,MAAM,eAAe,QAA+C,CAAC;AAC5E,eAAO,MAAM,eAAe,QAA+C,CAAC;AAC5E,eAAO,MAAM,gBAAgB,QAAgD,CAAC;AAC9E,eAAO,MAAM,gBAAgB,QAAgD,CAAC;AAC9E,eAAO,MAAM,YAAY,QAA2C,CAAC;AACrE,eAAO,MAAM,QAAQ,QAAuC,CAAC;AAC7D,eAAO,MAAM,SAAS,QAAwC,CAAC;AAC/D,eAAO,MAAM,SAAS,QAAwC,CAAC;AAC/D,eAAO,MAAM,WAAW,QAA0C,CAAC;AACnE,eAAO,MAAM,WAAW,QAA0C,CAAC;AACnE,eAAO,MAAM,iBAAiB,QAAiD,CAAC;AAChF,eAAO,MAAM,eAAe,QAA+C,CAAC;AAC5E,eAAO,MAAM,oBAAoB,QAEhC,CAAC;AACF,eAAO,MAAM,qBAAqB,QAEjC,CAAC;AACF,eAAO,MAAM,mBAAmB,QAE/B,CAAC;AACF,eAAO,MAAM,mBAAmB,QAE/B,CAAC;AACF,eAAO,MAAM,gBAAgB,QAAgD,CAAC;AAC9E,eAAO,MAAM,aAAa,QAA4C,CAAC;AACvE,eAAO,MAAM,oBAAoB,QAAoD,CAAC;AAGtF,eAAO,MAAM,kBAAkB,QAA2C,CAAC;AAC3E,eAAO,MAAM,mBAAmB,QAA4C,CAAC;AAC7E,eAAO,MAAM,UAAU,QAAkC,CAAC;AAC1D,eAAO,MAAM,gBAAgB,QAAyC,CAAC;AACvE,eAAO,MAAM,gBAAgB,QAAyC,CAAC;AAGvE,eAAO,MAAM,kBAAkB,QAA8B,CAAC;AAC9D,eAAO,MAAM,kBAAkB,QAA8B,CAAC;AAC9D,eAAO,MAAM,iBAAiB,QAAgC,CAAC;AAE/D;;GAEG;AACH,wBAAgB,aAAa,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/templates/trellis/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAcH,eAAO,MAAM,WAAW,QAAsC,CAAC;AAG/D,eAAO,MAAM,UAAU,QAA6C,CAAC;AACrE,eAAO,MAAM,WAAW,QAA0C,CAAC;AACnE,eAAO,MAAM,eAAe,QAA8C,CAAC;AAC3E,eAAO,MAAM,gBAAgB,QAAgD,CAAC;AAC9E,eAAO,MAAM,eAAe,QAA+C,CAAC;AAC5E,eAAO,MAAM,eAAe,QAA+C,CAAC;AAC5E,eAAO,MAAM,gBAAgB,QAAgD,CAAC;AAC9E,eAAO,MAAM,gBAAgB,QAAgD,CAAC;AAC9E,eAAO,MAAM,YAAY,QAA2C,CAAC;AACrE,eAAO,MAAM,QAAQ,QAAuC,CAAC;AAC7D,eAAO,MAAM,SAAS,QAAwC,CAAC;AAC/D,eAAO,MAAM,SAAS,QAAwC,CAAC;AAC/D,eAAO,MAAM,WAAW,QAA0C,CAAC;AACnE,eAAO,MAAM,WAAW,QAA0C,CAAC;AACnE,eAAO,MAAM,iBAAiB,QAAiD,CAAC;AAChF,eAAO,MAAM,eAAe,QAA+C,CAAC;AAC5E,eAAO,MAAM,oBAAoB,QAEhC,CAAC;AACF,eAAO,MAAM,qBAAqB,QAEjC,CAAC;AACF,eAAO,MAAM,mBAAmB,QAE/B,CAAC;AACF,eAAO,MAAM,mBAAmB,QAE/B,CAAC;AACF,eAAO,MAAM,gBAAgB,QAAgD,CAAC;AAC9E,eAAO,MAAM,kBAAkB,QAAkD,CAAC;AAClF,eAAO,MAAM,aAAa,QAA4C,CAAC;AACvE,eAAO,MAAM,oBAAoB,QAAoD,CAAC;AAGtF,eAAO,MAAM,kBAAkB,QAA2C,CAAC;AAC3E,eAAO,MAAM,mBAAmB,QAA4C,CAAC;AAC7E,eAAO,MAAM,UAAU,QAAkC,CAAC;AAC1D,eAAO,MAAM,gBAAgB,QAAyC,CAAC;AACvE,eAAO,MAAM,gBAAgB,QAAyC,CAAC;AAGvE,eAAO,MAAM,kBAAkB,QAA8B,CAAC;AAC9D,eAAO,MAAM,kBAAkB,QAA8B,CAAC;AAC9D,eAAO,MAAM,iBAAiB,QAAgC,CAAC;AAE/D;;GAEG;AACH,wBAAgB,aAAa,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAwCnD"}
|
|
@@ -47,6 +47,7 @@ export const commonPackagesContext = readTemplate("scripts/common/packages_conte
|
|
|
47
47
|
export const commonWorkflowPhase = readTemplate("scripts/common/workflow_phase.py");
|
|
48
48
|
export const commonTrellisConfig = readTemplate("scripts/common/trellis_config.py");
|
|
49
49
|
export const commonSafeCommit = readTemplate("scripts/common/safe_commit.py");
|
|
50
|
+
export const commonWorktreeSync = readTemplate("scripts/common/worktree_sync.py");
|
|
50
51
|
export const trellisSwitch = readTemplate("scripts/trellis_switch.py");
|
|
51
52
|
export const assertTrellisEnabled = readTemplate("scripts/assert_trellis_enabled.py");
|
|
52
53
|
// Python scripts - main
|
|
@@ -88,6 +89,7 @@ export function getAllScripts() {
|
|
|
88
89
|
scripts.set("common/workflow_phase.py", commonWorkflowPhase);
|
|
89
90
|
scripts.set("common/trellis_config.py", commonTrellisConfig);
|
|
90
91
|
scripts.set("common/safe_commit.py", commonSafeCommit);
|
|
92
|
+
scripts.set("common/worktree_sync.py", commonWorktreeSync);
|
|
91
93
|
// Main
|
|
92
94
|
scripts.set("get_developer.py", getDeveloperScript);
|
|
93
95
|
scripts.set("init_developer.py", initDeveloperScript);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/templates/trellis/index.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/templates/trellis/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAClD,MAAM,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;AAEtC,SAAS,YAAY,CAAC,YAAoB;IACxC,OAAO,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,YAAY,CAAC,EAAE,OAAO,CAAC,CAAC;AAC9D,CAAC;AAED,gCAAgC;AAChC,MAAM,CAAC,MAAM,WAAW,GAAG,YAAY,CAAC,qBAAqB,CAAC,CAAC;AAE/D,0BAA0B;AAC1B,MAAM,CAAC,MAAM,UAAU,GAAG,YAAY,CAAC,4BAA4B,CAAC,CAAC;AACrE,MAAM,CAAC,MAAM,WAAW,GAAG,YAAY,CAAC,yBAAyB,CAAC,CAAC;AACnE,MAAM,CAAC,MAAM,eAAe,GAAG,YAAY,CAAC,6BAA6B,CAAC,CAAC;AAC3E,MAAM,CAAC,MAAM,gBAAgB,GAAG,YAAY,CAAC,+BAA+B,CAAC,CAAC;AAC9E,MAAM,CAAC,MAAM,eAAe,GAAG,YAAY,CAAC,8BAA8B,CAAC,CAAC;AAC5E,MAAM,CAAC,MAAM,eAAe,GAAG,YAAY,CAAC,8BAA8B,CAAC,CAAC;AAC5E,MAAM,CAAC,MAAM,gBAAgB,GAAG,YAAY,CAAC,+BAA+B,CAAC,CAAC;AAC9E,MAAM,CAAC,MAAM,gBAAgB,GAAG,YAAY,CAAC,+BAA+B,CAAC,CAAC;AAC9E,MAAM,CAAC,MAAM,YAAY,GAAG,YAAY,CAAC,0BAA0B,CAAC,CAAC;AACrE,MAAM,CAAC,MAAM,QAAQ,GAAG,YAAY,CAAC,sBAAsB,CAAC,CAAC;AAC7D,MAAM,CAAC,MAAM,SAAS,GAAG,YAAY,CAAC,uBAAuB,CAAC,CAAC;AAC/D,MAAM,CAAC,MAAM,SAAS,GAAG,YAAY,CAAC,uBAAuB,CAAC,CAAC;AAC/D,MAAM,CAAC,MAAM,WAAW,GAAG,YAAY,CAAC,yBAAyB,CAAC,CAAC;AACnE,MAAM,CAAC,MAAM,WAAW,GAAG,YAAY,CAAC,yBAAyB,CAAC,CAAC;AACnE,MAAM,CAAC,MAAM,iBAAiB,GAAG,YAAY,CAAC,gCAAgC,CAAC,CAAC;AAChF,MAAM,CAAC,MAAM,eAAe,GAAG,YAAY,CAAC,8BAA8B,CAAC,CAAC;AAC5E,MAAM,CAAC,MAAM,oBAAoB,GAAG,YAAY,CAC9C,mCAAmC,CACpC,CAAC;AACF,MAAM,CAAC,MAAM,qBAAqB,GAAG,YAAY,CAC/C,oCAAoC,CACrC,CAAC;AACF,MAAM,CAAC,MAAM,mBAAmB,GAAG,YAAY,CAC7C,kCAAkC,CACnC,CAAC;AACF,MAAM,CAAC,MAAM,mBAAmB,GAAG,YAAY,CAC7C,kCAAkC,CACnC,CAAC;AACF,MAAM,CAAC,MAAM,gBAAgB,GAAG,YAAY,CAAC,+BAA+B,CAAC,CAAC;AAC9E,MAAM,CAAC,MAAM,kBAAkB,GAAG,YAAY,CAAC,iCAAiC,CAAC,CAAC;AAClF,MAAM,CAAC,MAAM,aAAa,GAAG,YAAY,CAAC,2BAA2B,CAAC,CAAC;AACvE,MAAM,CAAC,MAAM,oBAAoB,GAAG,YAAY,CAAC,mCAAmC,CAAC,CAAC;AAEtF,wBAAwB;AACxB,MAAM,CAAC,MAAM,kBAAkB,GAAG,YAAY,CAAC,0BAA0B,CAAC,CAAC;AAC3E,MAAM,CAAC,MAAM,mBAAmB,GAAG,YAAY,CAAC,2BAA2B,CAAC,CAAC;AAC7E,MAAM,CAAC,MAAM,UAAU,GAAG,YAAY,CAAC,iBAAiB,CAAC,CAAC;AAC1D,MAAM,CAAC,MAAM,gBAAgB,GAAG,YAAY,CAAC,wBAAwB,CAAC,CAAC;AACvE,MAAM,CAAC,MAAM,gBAAgB,GAAG,YAAY,CAAC,wBAAwB,CAAC,CAAC;AAEvE,sBAAsB;AACtB,MAAM,CAAC,MAAM,kBAAkB,GAAG,YAAY,CAAC,aAAa,CAAC,CAAC;AAC9D,MAAM,CAAC,MAAM,kBAAkB,GAAG,YAAY,CAAC,aAAa,CAAC,CAAC;AAC9D,MAAM,CAAC,MAAM,iBAAiB,GAAG,YAAY,CAAC,eAAe,CAAC,CAAC;AAE/D;;GAEG;AACH,MAAM,UAAU,aAAa;IAC3B,MAAM,OAAO,GAAG,IAAI,GAAG,EAAkB,CAAC;IAE1C,eAAe;IACf,OAAO,CAAC,GAAG,CAAC,aAAa,EAAE,WAAW,CAAC,CAAC;IAExC,SAAS;IACT,OAAO,CAAC,GAAG,CAAC,oBAAoB,EAAE,UAAU,CAAC,CAAC;IAC9C,OAAO,CAAC,GAAG,CAAC,iBAAiB,EAAE,WAAW,CAAC,CAAC;IAC5C,OAAO,CAAC,GAAG,CAAC,qBAAqB,EAAE,eAAe,CAAC,CAAC;IACpD,OAAO,CAAC,GAAG,CAAC,uBAAuB,EAAE,gBAAgB,CAAC,CAAC;IACvD,OAAO,CAAC,GAAG,CAAC,sBAAsB,EAAE,eAAe,CAAC,CAAC;IACrD,OAAO,CAAC,GAAG,CAAC,sBAAsB,EAAE,eAAe,CAAC,CAAC;IACrD,OAAO,CAAC,GAAG,CAAC,uBAAuB,EAAE,gBAAgB,CAAC,CAAC;IACvD,OAAO,CAAC,GAAG,CAAC,uBAAuB,EAAE,gBAAgB,CAAC,CAAC;IACvD,OAAO,CAAC,GAAG,CAAC,kBAAkB,EAAE,YAAY,CAAC,CAAC;IAC9C,OAAO,CAAC,GAAG,CAAC,cAAc,EAAE,QAAQ,CAAC,CAAC;IACtC,OAAO,CAAC,GAAG,CAAC,eAAe,EAAE,SAAS,CAAC,CAAC;IACxC,OAAO,CAAC,GAAG,CAAC,eAAe,EAAE,SAAS,CAAC,CAAC;IACxC,OAAO,CAAC,GAAG,CAAC,iBAAiB,EAAE,WAAW,CAAC,CAAC;IAC5C,OAAO,CAAC,GAAG,CAAC,iBAAiB,EAAE,WAAW,CAAC,CAAC;IAC5C,OAAO,CAAC,GAAG,CAAC,wBAAwB,EAAE,iBAAiB,CAAC,CAAC;IACzD,OAAO,CAAC,GAAG,CAAC,sBAAsB,EAAE,eAAe,CAAC,CAAC;IACrD,OAAO,CAAC,GAAG,CAAC,2BAA2B,EAAE,oBAAoB,CAAC,CAAC;IAC/D,OAAO,CAAC,GAAG,CAAC,4BAA4B,EAAE,qBAAqB,CAAC,CAAC;IACjE,OAAO,CAAC,GAAG,CAAC,0BAA0B,EAAE,mBAAmB,CAAC,CAAC;IAC7D,OAAO,CAAC,GAAG,CAAC,0BAA0B,EAAE,mBAAmB,CAAC,CAAC;IAC7D,OAAO,CAAC,GAAG,CAAC,uBAAuB,EAAE,gBAAgB,CAAC,CAAC;IACvD,OAAO,CAAC,GAAG,CAAC,yBAAyB,EAAE,kBAAkB,CAAC,CAAC;IAE3D,OAAO;IACP,OAAO,CAAC,GAAG,CAAC,kBAAkB,EAAE,kBAAkB,CAAC,CAAC;IACpD,OAAO,CAAC,GAAG,CAAC,mBAAmB,EAAE,mBAAmB,CAAC,CAAC;IACtD,OAAO,CAAC,GAAG,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;IACnC,OAAO,CAAC,GAAG,CAAC,gBAAgB,EAAE,gBAAgB,CAAC,CAAC;IAChD,OAAO,CAAC,GAAG,CAAC,gBAAgB,EAAE,gBAAgB,CAAC,CAAC;IAChD,OAAO,CAAC,GAAG,CAAC,mBAAmB,EAAE,aAAa,CAAC,CAAC;IAChD,OAAO,CAAC,GAAG,CAAC,2BAA2B,EAAE,oBAAoB,CAAC,CAAC;IAE/D,OAAO,OAAO,CAAC;AACjB,CAAC"}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import shutil
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from common.git import run_git
|
|
8
|
+
|
|
9
|
+
DIR_WORKFLOW = ".trellis"
|
|
10
|
+
WORKTREE_PARENT_DIR = ".trellis"
|
|
11
|
+
WORKTREE_ROOT_DIR = "trellis-worktrees"
|
|
12
|
+
RUNTIME_BUNDLE_FILES = (
|
|
13
|
+
".trellis/workflow.md",
|
|
14
|
+
".trellis/config.yaml",
|
|
15
|
+
".trellis/.gitignore",
|
|
16
|
+
)
|
|
17
|
+
RUNTIME_BUNDLE_DIRS = (
|
|
18
|
+
".trellis/scripts",
|
|
19
|
+
)
|
|
20
|
+
PLANNING_FILE_NAMES = (
|
|
21
|
+
"task.json",
|
|
22
|
+
"prd.md",
|
|
23
|
+
"design.md",
|
|
24
|
+
"implement.md",
|
|
25
|
+
"implement.jsonl",
|
|
26
|
+
"check.jsonl",
|
|
27
|
+
)
|
|
28
|
+
PLANNING_DIR_NAMES = ("research",)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _remove_path(path: Path) -> None:
|
|
32
|
+
if path.is_symlink() or path.is_file():
|
|
33
|
+
path.unlink()
|
|
34
|
+
return
|
|
35
|
+
if path.is_dir():
|
|
36
|
+
shutil.rmtree(path)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _hash_file(path: Path) -> str:
|
|
40
|
+
digest = hashlib.sha256()
|
|
41
|
+
with path.open("rb") as handle:
|
|
42
|
+
for chunk in iter(lambda: handle.read(65536), b""):
|
|
43
|
+
digest.update(chunk)
|
|
44
|
+
return digest.hexdigest()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _snapshot_dir(path: Path) -> dict[str, str]:
|
|
48
|
+
if not path.is_dir():
|
|
49
|
+
return {}
|
|
50
|
+
snapshot: dict[str, str] = {}
|
|
51
|
+
for child in sorted(path.rglob("*")):
|
|
52
|
+
if child.is_file():
|
|
53
|
+
snapshot[child.relative_to(path).as_posix()] = _hash_file(child)
|
|
54
|
+
return snapshot
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _same_file(src: Path, dst: Path) -> bool:
|
|
58
|
+
return src.is_file() and dst.is_file() and _hash_file(src) == _hash_file(dst)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _same_tree(src: Path, dst: Path) -> bool:
|
|
62
|
+
return src.is_dir() and dst.is_dir() and _snapshot_dir(src) == _snapshot_dir(dst)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _copy_file(src: Path, dst: Path) -> None:
|
|
66
|
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
67
|
+
shutil.copy2(src, dst)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _copy_tree(src: Path, dst: Path) -> None:
|
|
71
|
+
if dst.exists():
|
|
72
|
+
_remove_path(dst)
|
|
73
|
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
74
|
+
shutil.copytree(src, dst)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def task_dir(repo_root: Path, task_dir_name: str) -> Path:
|
|
78
|
+
return repo_root / DIR_WORKFLOW / "tasks" / task_dir_name
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def detect_trellis_managed_worktree(repo_root: Path) -> tuple[Path, str] | None:
|
|
82
|
+
try:
|
|
83
|
+
if repo_root.parent.name != WORKTREE_ROOT_DIR:
|
|
84
|
+
return None
|
|
85
|
+
if repo_root.parent.parent.name != WORKTREE_PARENT_DIR:
|
|
86
|
+
return None
|
|
87
|
+
main_root = repo_root.parent.parent.parent
|
|
88
|
+
if not (main_root / ".git").exists():
|
|
89
|
+
return None
|
|
90
|
+
return main_root, repo_root.name
|
|
91
|
+
except Exception:
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def infer_managed_worktree_task(repo_root: Path) -> str | None:
|
|
96
|
+
detected = detect_trellis_managed_worktree(repo_root)
|
|
97
|
+
if not detected:
|
|
98
|
+
return None
|
|
99
|
+
_, task_dir_name = detected
|
|
100
|
+
if not task_dir(repo_root, task_dir_name).is_dir():
|
|
101
|
+
return None
|
|
102
|
+
return f".trellis/tasks/{task_dir_name}"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def resolve_shared_worktree_roots(
|
|
106
|
+
repo_root: Path,
|
|
107
|
+
task_dir_name: str,
|
|
108
|
+
) -> tuple[Path, Path] | None:
|
|
109
|
+
detected = detect_trellis_managed_worktree(repo_root)
|
|
110
|
+
if detected:
|
|
111
|
+
main_root, inferred_task_dir_name = detected
|
|
112
|
+
if inferred_task_dir_name != task_dir_name:
|
|
113
|
+
return None
|
|
114
|
+
return main_root, repo_root
|
|
115
|
+
|
|
116
|
+
main_root = repo_root
|
|
117
|
+
worktree_root = repo_root / WORKTREE_PARENT_DIR / WORKTREE_ROOT_DIR / task_dir_name
|
|
118
|
+
if not worktree_root.exists():
|
|
119
|
+
return None
|
|
120
|
+
if not (main_root / DIR_WORKFLOW / "scripts").is_dir():
|
|
121
|
+
return None
|
|
122
|
+
return main_root, worktree_root
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def task_snapshot(task_dir_path: Path) -> dict[str, str]:
|
|
126
|
+
snapshot: dict[str, str] = {}
|
|
127
|
+
for name in PLANNING_FILE_NAMES:
|
|
128
|
+
file_path = task_dir_path / name
|
|
129
|
+
if file_path.is_file():
|
|
130
|
+
snapshot[name] = _hash_file(file_path)
|
|
131
|
+
for name in PLANNING_DIR_NAMES:
|
|
132
|
+
dir_path = task_dir_path / name
|
|
133
|
+
if not dir_path.is_dir():
|
|
134
|
+
continue
|
|
135
|
+
for child in sorted(dir_path.rglob("*")):
|
|
136
|
+
if child.is_file():
|
|
137
|
+
snapshot[child.relative_to(task_dir_path).as_posix()] = _hash_file(child)
|
|
138
|
+
return snapshot
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def has_any_task_artifact(task_dir_path: Path) -> bool:
|
|
142
|
+
for name in PLANNING_FILE_NAMES:
|
|
143
|
+
if (task_dir_path / name).is_file():
|
|
144
|
+
return True
|
|
145
|
+
for name in PLANNING_DIR_NAMES:
|
|
146
|
+
if (task_dir_path / name).exists():
|
|
147
|
+
return True
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def sync_runtime_bundle(main_root: Path, worktree_root: Path) -> list[str]:
|
|
152
|
+
synced: list[str] = []
|
|
153
|
+
for relative_path in RUNTIME_BUNDLE_FILES:
|
|
154
|
+
src = main_root / relative_path
|
|
155
|
+
dst = worktree_root / relative_path
|
|
156
|
+
if not src.is_file() or _same_file(src, dst):
|
|
157
|
+
continue
|
|
158
|
+
_copy_file(src, dst)
|
|
159
|
+
synced.append(relative_path)
|
|
160
|
+
for relative_path in RUNTIME_BUNDLE_DIRS:
|
|
161
|
+
src = main_root / relative_path
|
|
162
|
+
dst = worktree_root / relative_path
|
|
163
|
+
if not src.is_dir() or _same_tree(src, dst):
|
|
164
|
+
continue
|
|
165
|
+
_copy_tree(src, dst)
|
|
166
|
+
synced.append(relative_path)
|
|
167
|
+
return synced
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def sync_task_snapshot(main_root: Path, worktree_root: Path, task_dir_name: str) -> list[str]:
|
|
171
|
+
source_task_dir = task_dir(main_root, task_dir_name)
|
|
172
|
+
target_task_dir = task_dir(worktree_root, task_dir_name)
|
|
173
|
+
if not source_task_dir.is_dir():
|
|
174
|
+
return []
|
|
175
|
+
|
|
176
|
+
synced: list[str] = []
|
|
177
|
+
for name in PLANNING_FILE_NAMES:
|
|
178
|
+
src = source_task_dir / name
|
|
179
|
+
if not src.is_file():
|
|
180
|
+
continue
|
|
181
|
+
dst = target_task_dir / name
|
|
182
|
+
if _same_file(src, dst):
|
|
183
|
+
continue
|
|
184
|
+
_copy_file(src, dst)
|
|
185
|
+
synced.append(name)
|
|
186
|
+
|
|
187
|
+
for name in PLANNING_DIR_NAMES:
|
|
188
|
+
src = source_task_dir / name
|
|
189
|
+
if not src.is_dir():
|
|
190
|
+
continue
|
|
191
|
+
dst = target_task_dir / name
|
|
192
|
+
if _same_tree(src, dst):
|
|
193
|
+
continue
|
|
194
|
+
_copy_tree(src, dst)
|
|
195
|
+
synced.append(name)
|
|
196
|
+
|
|
197
|
+
return synced
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def collect_task_drift(main_root: Path, worktree_root: Path, task_dir_name: str) -> list[str]:
|
|
201
|
+
source_snapshot = task_snapshot(task_dir(main_root, task_dir_name))
|
|
202
|
+
target_snapshot = task_snapshot(task_dir(worktree_root, task_dir_name))
|
|
203
|
+
keys = sorted(set(source_snapshot) | set(target_snapshot))
|
|
204
|
+
return [key for key in keys if source_snapshot.get(key) != target_snapshot.get(key)]
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _is_managed_worktree_path(path_str: str, task_dir_name: str) -> bool:
|
|
208
|
+
normalized = path_str.replace("\\", "/").strip().strip("/")
|
|
209
|
+
|
|
210
|
+
for relative_path in RUNTIME_BUNDLE_FILES:
|
|
211
|
+
runtime_path = relative_path.strip("/")
|
|
212
|
+
if normalized == runtime_path:
|
|
213
|
+
return True
|
|
214
|
+
for relative_path in RUNTIME_BUNDLE_DIRS:
|
|
215
|
+
runtime_dir = relative_path.strip("/")
|
|
216
|
+
if normalized == runtime_dir or normalized.startswith(runtime_dir + "/"):
|
|
217
|
+
return True
|
|
218
|
+
|
|
219
|
+
task_root = f".trellis/tasks/{task_dir_name}"
|
|
220
|
+
if normalized in (".trellis/tasks", task_root):
|
|
221
|
+
return True
|
|
222
|
+
task_prefix = task_root + "/"
|
|
223
|
+
if not normalized.startswith(task_prefix):
|
|
224
|
+
return False
|
|
225
|
+
task_relative = normalized[len(task_prefix):]
|
|
226
|
+
if not task_relative:
|
|
227
|
+
return True
|
|
228
|
+
if task_relative in PLANNING_FILE_NAMES:
|
|
229
|
+
return True
|
|
230
|
+
for dir_name in PLANNING_DIR_NAMES:
|
|
231
|
+
if task_relative == dir_name or task_relative.startswith(dir_name + "/"):
|
|
232
|
+
return True
|
|
233
|
+
return False
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _changed_paths_from_git_status(worktree_root: Path) -> list[str]:
|
|
237
|
+
code, stdout, _ = run_git(
|
|
238
|
+
["status", "--porcelain", "-z", "--untracked-files=all"],
|
|
239
|
+
cwd=worktree_root,
|
|
240
|
+
)
|
|
241
|
+
if code != 0:
|
|
242
|
+
return []
|
|
243
|
+
|
|
244
|
+
changed_paths: list[str] = []
|
|
245
|
+
entries = stdout.split("\0")
|
|
246
|
+
index = 0
|
|
247
|
+
while index < len(entries):
|
|
248
|
+
entry = entries[index]
|
|
249
|
+
if not entry:
|
|
250
|
+
index += 1
|
|
251
|
+
continue
|
|
252
|
+
|
|
253
|
+
status = entry[:2]
|
|
254
|
+
if len(entry) > 3:
|
|
255
|
+
changed_paths.append(entry[3:].replace("\\", "/"))
|
|
256
|
+
|
|
257
|
+
if "R" in status or "C" in status:
|
|
258
|
+
index += 1
|
|
259
|
+
if index < len(entries) and entries[index]:
|
|
260
|
+
changed_paths.append(entries[index].replace("\\", "/"))
|
|
261
|
+
|
|
262
|
+
index += 1
|
|
263
|
+
|
|
264
|
+
return changed_paths
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def worktree_has_local_code_changes(worktree_root: Path, task_dir_name: str) -> bool:
|
|
268
|
+
if not worktree_root.exists():
|
|
269
|
+
return False
|
|
270
|
+
for relative_path in _changed_paths_from_git_status(worktree_root):
|
|
271
|
+
if not _is_managed_worktree_path(relative_path, task_dir_name):
|
|
272
|
+
return True
|
|
273
|
+
return False
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trellis-hgl",
|
|
3
|
-
"version": "0.6.0-beta.
|
|
3
|
+
"version": "0.6.0-beta.29",
|
|
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",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"inquirer": "^9.3.7",
|
|
35
35
|
"undici": "^6.21.0",
|
|
36
36
|
"zod": "^4.4.2",
|
|
37
|
-
"trellis-hgl-core": "0.6.0-beta.
|
|
37
|
+
"trellis-hgl-core": "0.6.0-beta.29"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@eslint/js": "^9.18.0",
|