trellis-hgl 0.6.0-beta.28 → 0.6.0-beta.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -225,7 +225,7 @@
225
225
  ### 安装
226
226
 
227
227
  ```bash
228
- npm install -g trellis-hgl@beta
228
+ npm install -g trellis-hgl@latest
229
229
  ```
230
230
 
231
231
  ### 卸载
@@ -241,7 +241,7 @@ npm uninstall -g trellis-hgl
241
241
  npm uninstall -g @mindfoldhq/trellis
242
242
 
243
243
  # 安装最新版 trellis-hgl
244
- npm install -g trellis-hgl@beta
244
+ npm install -g trellis-hgl@latest
245
245
 
246
246
  # 进入你自己的项目目录
247
247
  cd /path/to/your/project
@@ -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
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "version": "0.6.0-beta.30",
3
+ "description": "Beta 补丁:强化 Claude Code planning 的 research-first 提问路径,并用 AskUserQuestion 配置开发策略。",
4
+ "breaking": false,
5
+ "recommendMigrate": false,
6
+ "changelog": "**改进:**\n- feat(workflow): Claude Code planning 在进入 `trellis-brainstorm` 前,会先按 research-first gate 调用 `trellis-research` 调研代码,并把结果写入任务 `research/` 目录。后续问题必须基于调研证据提出,减少盲猜式、兜底式追问。\n- feat(workflow): 开发策略配置优先使用 Claude Code 原生 `AskUserQuestion`,用结构化单选/多选收集执行模式、工作区模式、开发流程、架构指导和 review-gate 选择。\n- docs(upgrade): 升级说明改为推荐 `trellis upgrade` 或 `npm install -g trellis-hgl@latest`,避免依赖当前滞后的 npm `beta` dist-tag。\n\n**说明:**\n- 现有项目执行 `trellis update` 后即可同步新的 workflow、skills 与启动提示。",
7
+ "migrations": [],
8
+ "notes": "安装此版本后请在已有项目中运行 `trellis update`,以同步 Claude Code planning 的 research-first 提问路径和 AskUserQuestion 开发策略配置提示。推荐使用 `trellis upgrade` 或 `npm install -g trellis-hgl@latest` 获取最新版本;不要依赖当前滞后的 `trellis-hgl@beta` dist-tag。"
9
+ }
@@ -37,7 +37,15 @@ Index files list the specific guideline docs to read when you actually start cod
37
37
  From Step 1 you know the current task and status. Check the task directory:
38
38
 
39
39
  - **Active task status `planning` + no `prd.md`** → Phase 1.1. Load the `trellis-brainstorm` skill.
40
- - **Active task status `planning` + `prd.md` exists** → stay in Phase 1. Lightweight tasks can be PRD-only; complex tasks need `design.md` + `implement.md`. For the Claude Code path, keep requirement clarification in `prd.md`, continue repository-first follow-up questioning by entering `trellis-grill-me`, and treat it as a required planning gate. Before `trellis-grill-me` is complete, do not enter development strategy decisions, do not create or complete `design.md` / `implement.md`, and do not run `task.py start`. Before `task.py start`, record the decisions in the task documents: development mode, branch vs worktree, default flow vs TDD, plus the task-level review-gate selection. Ask these in one `A.` / `B.` / `C.` style strategy block. New tasks must stamp that block with `Review-gate contract: explicit-selection-v1`. Complex tasks should keep that record in `implement.md` together with the enabled/disabled review gates and the preserved order for any enabled Claude review gates: `trellis-spec-review` → `trellis-code-review` → `trellis-code-architecture-review`. While the choice is still open, `Optional review gates status: pending` is allowed; before `task.py start`, replace it with `Optional review gates status: configured` plus explicit `Enabled optional review gates:` and `Disabled optional review gates:` lists for the selectable review gates (`trellis-spec-review`, `trellis-code-review`, `trellis-code-architecture-review`, `trellis-improve-codebase-architecture`, `trellis-merge-review`). If the user leaves all optional gates off, still record all five in the disabled list while keeping `trellis-check` fixed outside that set. Only tasks that entirely lack `Review-gate contract: explicit-selection-v1` count as legacy tasks and preserve the old behavior; if the marker exists but the configured enabled/disabled lists are missing, planning is incomplete and the task must not start. `trellis-improve-codebase-architecture` deep-review requires `trellis-code-architecture-review`; do not record or accept deep-review without that prerequisite gate. If the strategy is `subagent + worktree`, pin `./.trellis/trellis-worktrees/<task-dir-name>`. On Claude Code, that shared worktree is different from host `Agent(..., isolation: "worktree")`; when both collide, the hook removes the host isolation input before dispatch. If the strategy is TDD, record `trellis-tdd` as the reference flow. Also record whether to run pre-development architecture guidance in that same strategy block. If guidance is enabled, record `架构审查:enabled`, dispatch `trellis-improve-codebase-architecture` with `架构审查模式: guidance` before `task.py start`, and append its output to `design.md`, but do NOT implicitly enable `trellis-improve-codebase-architecture` deep-review; that gate still requires explicit selection in the task-level review-gate set.
40
+ - **Active task status `planning` + `prd.md` exists** → stay in Phase 1. Lightweight tasks can be PRD-only; complex tasks need `design.md` + `implement.md`. For the Claude Code path, keep requirement clarification in `prd.md`, continue repository-first follow-up questioning by entering `trellis-grill-me`, and treat it as a required planning gate. Before `trellis-grill-me` is complete, do not enter development strategy decisions, do not create or complete `design.md` / `implement.md`, and do not run `task.py start`.
41
+ - Before `task.py start`, record the decisions in the task documents: development mode, branch vs worktree, default flow vs TDD, pre-development architecture guidance, plus the task-level review-gate selection.
42
+ - On Claude Code, prefer the native `AskUserQuestion` tool for these development strategy decisions instead of a plain Markdown prompt. Each `AskUserQuestion` question must have 2–4 options. Split the decision into structured questions for development/workspace mode, default flow/TDD and architecture guidance, core review gates, and add-on review gates. Use `multiSelect: true` for review-gate questions.
43
+ - Core review gates are `trellis-spec-review`, `trellis-code-review`, and `trellis-code-architecture-review`; add-on review gates are `trellis-improve-codebase-architecture` and `trellis-merge-review`. Do not offer default review-gate packages; let the user directly select enabled gates, then write explicit enabled/disabled lists.
44
+ - If `AskUserQuestion` is unavailable, rejected, or the user chooses Other, fall back to a text `A.` / `B.` / `C.` option block or targeted free-text follow-up, but ask only for unresolved fields and never repeat answered choices unchanged.
45
+ - New tasks must stamp that block with `Review-gate contract: explicit-selection-v1`. Complex tasks should keep that record in `implement.md` together with the enabled/disabled review gates and the preserved order for any enabled Claude review gates: `trellis-spec-review` → `trellis-code-review` → `trellis-code-architecture-review`.
46
+ - While the choice is still open, `Optional review gates status: pending` is allowed; before `task.py start`, replace it with `Optional review gates status: configured` plus explicit `Enabled optional review gates:` and `Disabled optional review gates:` lists for the selectable review gates (`trellis-spec-review`, `trellis-code-review`, `trellis-code-architecture-review`, `trellis-improve-codebase-architecture`, `trellis-merge-review`). If the user leaves all optional gates off, still record all five in the disabled list while keeping `trellis-check` fixed outside that set.
47
+ - Only tasks that entirely lack `Review-gate contract: explicit-selection-v1` count as legacy tasks and preserve the old behavior; if the marker exists but the configured enabled/disabled lists are missing, planning is incomplete and the task must not start. `trellis-improve-codebase-architecture` deep-review requires `trellis-code-architecture-review`; do not record or accept deep-review without that prerequisite gate.
48
+ - If the strategy is `subagent + worktree`, pin `./.trellis/trellis-worktrees/<task-dir-name>`. On Claude Code, that shared worktree is different from host `Agent(..., isolation: "worktree")`; when both collide, the hook removes the host isolation input before dispatch. If the strategy is TDD, record `trellis-tdd` as the reference flow. If guidance is enabled, record `架构审查:enabled`, dispatch `trellis-improve-codebase-architecture` with `架构审查模式: guidance` before `task.py start`, and append its output to `design.md`, but do NOT implicitly enable `trellis-improve-codebase-architecture` deep-review; that gate still requires explicit selection in the task-level review-gate set.
41
49
  - **Active task status `in_progress`** → Phase 2 step 2.1. Load the step detail:
42
50
  ```bash
43
51
  {{PYTHON_CMD}} ./.trellis/scripts/get_context.py --mode phase --step 2.1 --platform {{CLI_FLAG}}
@@ -14,6 +14,14 @@ This is mandatory. Before asking the user a question, first check whether the an
14
14
 
15
15
  Do not ask the user to confirm facts that the repository can answer. Ask only for product intent, preference, scope, risk tolerance, or decisions that remain ambiguous after inspection.
16
16
 
17
+ ## Claude Code Research Gate
18
+
19
+ On the Claude Code path, feature additions, feature changes, and bug fixes that depend on repository facts must run `trellis-research` before the first `trellis-brainstorm` question. When this gate triggers, explicitly tell the user that repository evidence is required, `trellis-research` is running first, and questioning will resume from the findings. Research findings must be persisted to `{TASK_DIR}/research/` and used to frame the first question.
20
+
21
+ Do not trigger the pre-brainstorm research gate for pure conversation, capability or usage explanation, pure user preference choices, copy edits that do not depend on repository facts, explicitly local-only small tasks, or when the user explicitly asks to skip research and answer directly.
22
+
23
+ If a later user answer would materially change the current understanding of repository facts, pause the interview and run `trellis-research` again before asking the next question. If research is inconclusive, report what was checked, which evidence is missing, and which product intent or scope decision the user must provide before asking the question.
24
+
17
25
  ---
18
26
 
19
27
  Use this skill during Phase 1 planning to turn the user's request into clear requirements and planning artifacts.
@@ -49,7 +57,7 @@ Use a concise title from the user's request. Use a slug without a date prefix. `
49
57
  6. After each user answer, update `prd.md` before continuing.
50
58
  7. Once repository-answerable questions are exhausted, enter `trellis-grill-me` and tighten the remaining requirement gaps one question at a time, each with a recommendation, while still updating `prd.md` after each answer.
51
59
  8. On the Claude Code path, `trellis-grill-me` is a required planning gate. Before it is complete, do not switch into development strategy decisions, do not create or complete `design.md` / `implement.md`, and do not run `task.py start`.
52
- 9. Before implementation starts on the Claude Code path, record the development strategy decisions in the task documents: development mode, branch vs worktree, default flow vs TDD, and the task-level review-gate selection. Ask these in a single `A.` / `B.` / `C.` style option block. New tasks must stamp that strategy block with `Review-gate contract: explicit-selection-v1`. Lightweight tasks may keep that record in `prd.md`; complex tasks should keep it in `implement.md` together with the enabled/disabled review gates and the preserved execution order for any enabled Claude review gates: `trellis-spec-review` → `trellis-code-review` → `trellis-code-architecture-review`. The selectable review gates are `trellis-spec-review`, `trellis-code-review`, `trellis-code-architecture-review`, `trellis-improve-codebase-architecture`, and `trellis-merge-review`. While the choice is still open, `Optional review gates status: pending` is allowed; before `task.py start`, replace it with `Optional review gates status: configured` plus explicit `Enabled optional review gates:` and `Disabled optional review gates:` lists. If the user leaves all optional gates off, still record all five in the disabled list; `trellis-check` stays fixed outside this set. Only tasks that entirely lack `Review-gate contract: explicit-selection-v1` count as legacy tasks and preserve the old behavior; if the marker exists but the configured enabled/disabled lists are missing, planning is incomplete and the task must not start. `trellis-improve-codebase-architecture` deep-review requires `trellis-code-architecture-review`, so do not record or accept the deep-review gate without that prerequisite. If the strategy is `subagent + worktree`, pin `./.trellis/trellis-worktrees/<task-dir-name>`. If the strategy is TDD, record `trellis-tdd` as the reference flow. Also record whether to run pre-development architecture guidance in that same strategy block. If guidance is enabled, record `架构审查:enabled`, dispatch `trellis-improve-codebase-architecture` with `架构审查模式: guidance` before `task.py start`, and append its output to `design.md`, but do NOT implicitly enable `trellis-improve-codebase-architecture` deep-review; that gate still requires explicit selection in the task-level review-gate set.
60
+ 9. Before implementation starts on the Claude Code path, record the development strategy decisions in the task documents: development mode, branch vs worktree, default flow vs TDD, and the task-level review-gate selection. Prefer Claude Code's native `AskUserQuestion` tool for this development strategy decision instead of a plain Markdown prompt. Every `AskUserQuestion` question must have 2–4 options; do not put all five optional review gates in one question. Use structured questions for development/workspace mode, flow/architecture guidance, core review gates with `multiSelect: true`, and add-on review gates with `multiSelect: true`. Core review gates are `trellis-spec-review`, `trellis-code-review`, and `trellis-code-architecture-review`; add-on review gates are `trellis-improve-codebase-architecture` and `trellis-merge-review`. Do not offer default review-gate packages; let the user directly multi-select enabled gates, then write the explicit enabled/disabled lists. If `AskUserQuestion` is unavailable, rejected, or the user chooses Other, fall back to a text `A.` / `B.` / `C.` option block or targeted free-text follow-up, but ask only for unresolved fields and never repeat answered choices unchanged. New tasks must stamp that strategy block with `Review-gate contract: explicit-selection-v1`. Lightweight tasks may keep that record in `prd.md`; complex tasks should keep it in `implement.md` together with the enabled/disabled review gates and the preserved execution order for any enabled Claude review gates: `trellis-spec-review` → `trellis-code-review` → `trellis-code-architecture-review`. The selectable review gates are `trellis-spec-review`, `trellis-code-review`, `trellis-code-architecture-review`, `trellis-improve-codebase-architecture`, and `trellis-merge-review`. While the choice is still open, `Optional review gates status: pending` is allowed; before `task.py start`, replace it with `Optional review gates status: configured` plus explicit `Enabled optional review gates:` and `Disabled optional review gates:` lists. If the user leaves all optional gates off, still record all five in the disabled list; `trellis-check` stays fixed outside this set. Only tasks that entirely lack `Review-gate contract: explicit-selection-v1` count as legacy tasks and preserve the old behavior; if the marker exists but the configured enabled/disabled lists are missing, planning is incomplete and the task must not start. `trellis-improve-codebase-architecture` deep-review requires `trellis-code-architecture-review`, so do not record or accept the deep-review gate without that prerequisite. If the strategy is `subagent + worktree`, pin `./.trellis/trellis-worktrees/<task-dir-name>`. If the strategy is TDD, record `trellis-tdd` as the reference flow. Also record whether to run pre-development architecture guidance in that same strategy block. If guidance is enabled, record `架构审查:enabled`, dispatch `trellis-improve-codebase-architecture` with `架构审查模式: guidance` before `task.py start`, and append its output to `design.md`, but do NOT implicitly enable `trellis-improve-codebase-architecture` deep-review; that gate still requires explicit selection in the task-level review-gate set.
53
61
  10. For complex tasks, create or update `design.md` and `implement.md` only after `trellis-grill-me` is complete and implementation is about to start.
54
62
 
55
63
  Do not invent a project-specific product/spec hierarchy. If the repository already has product, domain, or spec docs, use them. If it does not, proceed with the evidence that exists.
@@ -17,6 +17,14 @@ Use this skill only when:
17
17
 
18
18
  Do **not** use this skill for questions the codebase can answer directly.
19
19
 
20
+ ## Claude Code Research Gate
21
+
22
+ On the Claude Code path, this skill may only continue after the active planning thread has already run any required `trellis-research` pass and persisted findings to `{TASK_DIR}/research/` for repository-dependent feature additions, feature changes, or bug fixes.
23
+
24
+ Before each question, check whether the remaining gap is still a repository fact. If it is, stop the interview, explicitly tell the user that repository evidence is required, run `trellis-research`, persist the result, and then continue from the evidence. If research is inconclusive, report what was checked, which evidence is missing, and the exact product intent or scope decision needed from the user.
25
+
26
+ If a user answer would materially change the current understanding of repository facts, run `trellis-research` again before asking the next follow-up question.
27
+
20
28
  ## Interview Contract
21
29
 
22
30
  - Ask one question at a time.
@@ -47,3 +55,4 @@ By the time this skill is done:
47
55
  - `prd.md` has testable acceptance criteria
48
56
  - unresolved questions are truly strategic, not factual
49
57
  - implementation can move on to development mode / worktree / TDD decisions
58
+ - the next Claude Code development strategy decision should prefer `AskUserQuestion`; each question must have 2–4 options, review gates must be split into core and add-on `multiSelect: true` questions, and text fallback should ask only unresolved fields
@@ -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
@@ -185,10 +185,12 @@ Lightweight: `prd.md` can be enough. Complex: finish `prd.md`, `design.md`, and
185
185
  Multi-deliverable scope: consider a parent task plus independently verifiable child tasks; dependencies must be written in child artifacts, not implied by tree position.
186
186
  Sub-agent mode: curate `implement.jsonl` and `check.jsonl` as spec/research manifests before start.
187
187
 
188
- Planning order for this Claude Code path: `task.py create` `trellis-brainstorm` `trellis-grill-me` development strategy decision.
188
+ Claude Code research-first gate: for feature additions, feature changes, and bug fixes that depend on repository facts, run `trellis-research` before the first `trellis-brainstorm` question and persist findings to `{TASK_DIR}/research/`. When this gate triggers, explicitly tell the user that repository evidence is required, `trellis-research` is running first, and questioning will resume from the findings. Do not trigger this pre-brainstorm gate for pure conversation, capability or usage explanation, pure user preference choices, copy edits that do not depend on repository facts, explicitly local-only small tasks, or when the user explicitly asks to skip research and answer directly. If a later user answer would materially change the current understanding of repository facts, pause and run `trellis-research` again before the next question. If research is inconclusive, report what was checked, what evidence is missing, and which product intent or scope decision the user must provide before asking the question.
189
+ Planning order for this Claude Code path: `task.py create` → `trellis-research` when the research-first gate applies → `trellis-brainstorm` → `trellis-grill-me` → development strategy decision.
189
190
  `trellis-grill-me` is a required planning gate on this Claude Code path, not an optional suggestion.
190
191
  Before `trellis-grill-me` is complete, do not enter development strategy decisions, do not create or complete `design.md` / `implement.md`, and do not run `task.py start`.
191
192
  Do not enter development strategy decisions until `prd.md` has been tightened through repository-first clarification and one-question-at-a-time follow-up.
193
+ At the Claude Code development strategy decision point, prefer the native `AskUserQuestion` tool instead of plain Markdown prompts. Every `AskUserQuestion` question must have 2–4 options; do not put all five optional review gates in one question. Use structured questions for development/workspace mode, flow/architecture guidance, core review gates with `multiSelect: true`, and add-on review gates with `multiSelect: true`. Core review gates are `trellis-spec-review`, `trellis-code-review`, and `trellis-code-architecture-review`; add-on review gates are `trellis-improve-codebase-architecture` and `trellis-merge-review`. Do not offer default review-gate packages; let the user directly multi-select enabled gates, then write the explicit enabled/disabled lists. If `AskUserQuestion` is unavailable, rejected, or the user chooses Other, fall back to a text `A.` / `B.` / `C.` block or targeted free-text follow-up, but ask only for unresolved fields and never repeat answered choices unchanged.
192
194
  Before `task.py start`, record the development strategy decisions in the task documents. Complex tasks should store them in `implement.md`: development mode (current session / subagent), branch vs worktree, default flow vs TDD, plus a single `A.` / `B.` / `C.` style strategy block that records the task-level selection for `trellis-spec-review`, `trellis-code-review`, `trellis-code-architecture-review`, `trellis-improve-codebase-architecture`, and `trellis-merge-review`. New tasks must stamp that block with `Review-gate contract: explicit-selection-v1`. Use `Optional review gates status: pending` only while the choice is still open; before `task.py start`, replace it with `Optional review gates status: configured` plus explicit `Enabled optional review gates:` and `Disabled optional review gates:` lists. If the user does not select any optional gate, still record all five in the disabled list and keep `trellis-check` fixed outside the optional set. Only tasks that entirely lack `Review-gate contract: explicit-selection-v1` count as legacy tasks and preserve the old behavior; if the marker exists but the configured enabled/disabled lists are missing, planning is incomplete and the task must not start. `trellis-improve-codebase-architecture` deep-review requires `trellis-code-architecture-review`; do not record or accept deep-review without that prerequisite gate. If the strategy is `subagent + worktree`, pin the shared path to `./.trellis/trellis-worktrees/<task-dir-name>` and require every code-development subagent to use it. If the strategy is TDD, record `trellis-tdd` as the reference flow. Record whether to run pre-development architecture guidance in that same strategy block. If guidance is enabled, record `架构审查:enabled` in `implement.md`, dispatch `trellis-improve-codebase-architecture` with `架构审查模式: guidance` before `task.py start`, and append its output to `design.md`, but do NOT implicitly enable `trellis-improve-codebase-architecture` deep-review; that gate still requires explicit selection in the same task-level review-gate set.
193
195
  [/workflow-state:planning]
194
196
 
@@ -339,6 +341,14 @@ python3 ./.trellis/scripts/task.py create "<任务标题>" --slug <name>
339
341
 
340
342
  加载 `trellis-brainstorm` skill,并按该 skill 的指导与用户交互式澄清需求。
341
343
 
344
+ Claude Code research-first gate:
345
+ - 对新增功能、修改现有功能,以及依赖仓库事实的 bug fix,在第一个 `trellis-brainstorm` 问题前先分派 `trellis-research`,并把研究结果写入 `{TASK_DIR}/research/`。
346
+ - 触发该门槛时,先明确告诉用户当前问题需要仓库证据、正在先执行 `trellis-research`,并会基于调研结果继续提问或收敛需求。
347
+ - 排除:纯对话、能力说明、用法解释、纯用户偏好选择、不依赖仓库事实的文案润色、明确只做局部处理的小任务,以及用户明确要求先别 research / 先直接问的请求。
348
+ - 第一次 research 后,问题必须基于 research 结果提出,而不是脱离仓库事实兜底猜测。
349
+ - 如果用户回答会明显影响“仓库当前事实”,继续追问或收敛需求前必须再次分派 `trellis-research`。
350
+ - 如果 research 无法找到足够证据,继续问用户前必须先报告已查证据、缺失证据和需要用户补充的具体意图或范围。
351
+
342
352
  brainstorm skill 会指导你:
343
353
  - 一次只问一个问题
344
354
  - 能靠调研回答的就先调研,不要先问用户
@@ -357,6 +367,13 @@ brainstorm skill 会指导你:
357
367
  - 启动拥有下一个交付物的子任务。Do not start the parent unless the parent itself has direct implementation work.
358
368
 
359
369
  在 Claude Code 路径进入实现前,把开发策略决策写入任务文档。
370
+ - 进入开发策略决策时,优先调用 Claude Code 原生 `AskUserQuestion` 工具,而不是只输出普通 Markdown 选项块。
371
+ - 每个 `AskUserQuestion` question 必须只有 2–4 options;不要把 5 个 optional review gates 放进同一个 question。
372
+ - 推荐拆分为:开发模式 / 工作区模式单选、默认流程 / TDD 与架构指导单选、核心 review gates 多选、附加 review gates 多选。
373
+ - 核心 review gates 问题使用 `multiSelect: true`,只包含 `trellis-spec-review`、`trellis-code-review`、`trellis-code-architecture-review`。
374
+ - 附加 review gates 问题使用 `multiSelect: true`,只包含 `trellis-improve-codebase-architecture` 和 `trellis-merge-review`。
375
+ - 不提供默认 review-gate 套餐;让用户直接多选要启用的 gates,再把未选项写入 disabled 列表。
376
+ - 如果 `AskUserQuestion` 不可用、被拒绝或用户选择 Other,退回文本 `A.` / `B.` / `C.` 选项块或定向自由文本补问,但只补齐未决字段,不要原样重复已回答选择。
360
377
  - 轻量任务可以把这些记录写在 `prd.md`。
361
378
  - 复杂任务应把这些策略、task-local review-gate 选择,以及任何已启用 Claude review gate 的保序执行顺序写在 `implement.md`。
362
379
  - 需要在同一个 `A.` / `B.` / `C.` 风格策略块里记录:当前会话 vs subagent、当前分支 vs worktree、默认流程 vs TDD、是否启用 pre-development architecture guidance,以及 `trellis-spec-review`、`trellis-code-review`、`trellis-code-architecture-review`、`trellis-improve-codebase-architecture`、`trellis-merge-review` 的任务级选择。
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.30",
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.30"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@eslint/js": "^9.18.0",