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.
@@ -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
- root = Path(repo_root).resolve()
120
+ worktree_sync = _load_worktree_sync(repo_root)
121
+ if worktree_sync is None:
122
+ return None
100
123
  try:
101
- if root.parent.name != WORKTREE_ROOT_DIR:
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
- task_dir_name = root.name
109
- task_dir = root / DIR_WORKFLOW / "tasks" / task_dir_name
110
- if not task_dir.is_dir():
111
- return None
112
- return f".trellis/tasks/{task_dir_name}"
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
- if has_shared_worktree_signal(repo_root, task_dir, tool_input, cwd):
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
- def _copy_tree(src: Path, dst: Path) -> None:
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 != WORKTREE_ROOT_DIR:
170
- return None
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
- return None
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 _worktree_has_local_code_changes(worktree_root: Path, task_dir_name: str) -> bool:
297
- if not worktree_root.exists():
298
- return False
299
- for child in sorted(worktree_root.rglob("*")):
300
- if child.is_dir():
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
- relative_path = child.relative_to(worktree_root).as_posix()
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
- detected = _detect_trellis_managed_worktree(project_dir)
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 = _sync_runtime_bundle(main_root, project_dir)
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 = _task_dir(project_dir, task_dir_name)
333
- if not _has_any_task_artifact(target_task_dir):
334
- planning_synced = _sync_task_snapshot(main_root, project_dir, task_dir_name)
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 = _collect_task_drift(main_root, project_dir, task_dir_name)
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 _worktree_has_local_code_changes(project_dir, task_dir_name):
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
- detected = _detect_trellis_managed_worktree(repo_root)
598
- if not detected:
417
+ worktree_sync = _load_worktree_sync(repo_root)
418
+ if worktree_sync is None:
599
419
  return None
600
- _, task_dir_name = detected
601
- task_dir = _task_dir(repo_root, task_dir_name)
602
- if not task_dir.is_dir():
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,CAuCnD"}
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":"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"}
@@ -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.28",
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.28"
37
+ "trellis-hgl-core": "0.6.0-beta.29"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@eslint/js": "^9.18.0",