@jahanxu/trellis 0.5.0 → 0.5.5

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.
Files changed (49) hide show
  1. package/dist/cli/index.js +1 -0
  2. package/dist/cli/index.js.map +1 -1
  3. package/dist/commands/init.d.ts +1 -0
  4. package/dist/commands/init.d.ts.map +1 -1
  5. package/dist/commands/init.js +98 -5
  6. package/dist/commands/init.js.map +1 -1
  7. package/dist/configurators/workflow.d.ts.map +1 -1
  8. package/dist/configurators/workflow.js +8 -58
  9. package/dist/configurators/workflow.js.map +1 -1
  10. package/dist/constants/paths.d.ts +0 -17
  11. package/dist/constants/paths.d.ts.map +1 -1
  12. package/dist/constants/paths.js +0 -19
  13. package/dist/constants/paths.js.map +1 -1
  14. package/dist/templates/claude/commands/trellis/handoff.md +56 -122
  15. package/dist/templates/claude/hooks/enforce-output-dir.py +115 -0
  16. package/dist/templates/claude/hooks/session-start.py +87 -166
  17. package/dist/templates/claude/settings.json +10 -0
  18. package/dist/templates/iflow/hooks/session-start.py +0 -171
  19. package/dist/templates/markdown/index.d.ts +0 -9
  20. package/dist/templates/markdown/index.d.ts.map +1 -1
  21. package/dist/templates/markdown/index.js +0 -10
  22. package/dist/templates/markdown/index.js.map +1 -1
  23. package/dist/templates/trellis/index.d.ts +9 -1
  24. package/dist/templates/trellis/index.d.ts.map +1 -1
  25. package/dist/templates/trellis/index.js +17 -2
  26. package/dist/templates/trellis/index.js.map +1 -1
  27. package/dist/templates/trellis/scripts/common/__init__.py +11 -0
  28. package/dist/templates/trellis/scripts/common/paths.py +1 -49
  29. package/dist/templates/trellis/scripts/common/roles.py +252 -0
  30. package/dist/templates/trellis/spec/roles/designer/index.md +49 -0
  31. package/dist/templates/trellis/spec/roles/frontend-impl/index.md +52 -0
  32. package/dist/templates/trellis/spec/roles/pm/index.md +44 -0
  33. package/dist/utils/template-hash.d.ts.map +1 -1
  34. package/dist/utils/template-hash.js +2 -0
  35. package/dist/utils/template-hash.js.map +1 -1
  36. package/package.json +2 -2
  37. package/dist/templates/claude/commands/trellis/pick-task.md +0 -145
  38. package/dist/templates/iflow/commands/trellis/handoff.md +0 -148
  39. package/dist/templates/iflow/commands/trellis/pick-task.md +0 -145
  40. package/dist/templates/markdown/spec/roles/designer/index.md.txt +0 -57
  41. package/dist/templates/markdown/spec/roles/designer/mock-data-standards.md.txt +0 -63
  42. package/dist/templates/markdown/spec/roles/designer/prototype-guidelines.md.txt +0 -49
  43. package/dist/templates/markdown/spec/roles/frontend-impl/api-integration.md.txt +0 -63
  44. package/dist/templates/markdown/spec/roles/frontend-impl/index.md.txt +0 -57
  45. package/dist/templates/markdown/spec/roles/frontend-impl/prototype-to-production.md.txt +0 -57
  46. package/dist/templates/markdown/spec/roles/pm/index.md.txt +0 -45
  47. package/dist/templates/markdown/spec/roles/pm/prd-template.md.txt +0 -64
  48. package/dist/templates/markdown/spec/roles/pm/requirement-checklist.md.txt +0 -43
  49. package/dist/templates/trellis/scripts/pool.py +0 -322
@@ -1,148 +1,82 @@
1
- # Handoff - Complete Task and Add to Pool
2
-
3
- Complete the current task, generate a HANDOFF.md document, and add the deliverable to the appropriate pool.
4
-
5
1
  ---
6
-
7
- ## Prerequisites
8
-
9
- - Must have a current task set (`.trellis/.current-task`)
10
- - Task must have deliverable files in the output directory
11
-
2
+ description: "Complete current phase and generate handoff document for downstream roles"
12
3
  ---
13
4
 
14
- ## Process `[AI]`
15
-
16
- ### Step 1: Read Current Task
17
-
18
- ```bash
19
- python3 ./.trellis/scripts/get_context.py
20
- ```
21
-
22
- Read the task's `task.json` to get:
23
- - `output_dir`: where deliverables are stored
24
- - `role`: which role completed this task
25
- - `title`: task title
26
- - `slug`: task slug / deliverable ID
27
-
28
- If no current task is set, inform the user and stop.
29
-
30
- ### Step 2: Check Deliverables
31
-
32
- Verify that the output directory exists and contains files:
33
-
34
- ```bash
35
- ls -la <output_dir>/
36
- ```
5
+ You are executing the **HANDOFF** workflow for a collaborative project.
37
6
 
38
- If empty or missing, warn the user:
39
- > "The output directory `<output_dir>` appears empty. Would you like to continue anyway?"
7
+ ## Step 1: Identify Context
40
8
 
41
- ### Step 3: Generate HANDOFF.md
9
+ 1. Read `.trellis/.developer` to get the current developer name
10
+ 2. Read `.trellis/roles.json` to determine the role and bound output directory
11
+ 3. If roles.json doesn't exist or current developer has no role mapping, abort with:
12
+ "Error: No role binding found. Run `trellis init -u <role>-<name> -d <dir>` first."
42
13
 
43
- Analyze all files in the output directory and generate a `HANDOFF.md` document.
14
+ ## Step 2: Check Deliverables
44
15
 
45
- **For PM role (requirements pool):**
16
+ 1. List all files in the bound output directory
17
+ 2. If the directory is empty, warn: "Output directory is empty. Are you sure you want to proceed?" and wait for confirmation
18
+ 3. Run `git diff --name-only` scoped to the output directory to identify recent changes
46
19
 
47
- ```markdown
48
- # {Title} - Requirements Handoff
20
+ ## Step 3: Generate HANDOFF.md
49
21
 
50
- ## Task Info
51
- - **Feature ID**: {slug}
52
- - **Title**: {title}
53
- - **Completed by**: {developer}
54
- - **Completed at**: {timestamp}
55
-
56
- ## Core Requirements
57
- (AI-generated summary of PRD key points, 2-3 paragraphs)
58
-
59
- ## Deliverable Files
60
- - `prd.md` - Product requirements document
61
- - (list all files)
62
-
63
- ## Key Design Points
64
- 1. (extracted from PRD)
65
- 2. ...
66
-
67
- ## Special Notes for Downstream
68
- - (important details the Designer should know)
69
- - (constraints, edge cases, specific UI requirements)
22
+ Create or overwrite `HANDOFF.md` **inside the output directory** with the following structure:
70
23
 
71
- ## Related Resources
72
- - (links if any)
73
- ```
74
-
75
- **For Designer role (prototypes pool):**
76
-
77
- ```markdown
78
- # {Title} - Prototype Handoff
24
+ ````
25
+ # {Feature/Task Name} - Handoff Document
79
26
 
80
27
  ## Task Info
81
- - **Feature ID**: {slug}
82
- - **Based on**: {source requirement}
83
- - **Completed by**: {developer}
84
- - **Completed at**: {timestamp}
28
+ - **Role**: {current role}
29
+ - **Developer**: {developer name}
30
+ - **Output Directory**: {bound directory}
31
+ - **Completed**: {current timestamp}
85
32
 
86
- ## Design Notes
87
- (AI-generated summary of design approach, 2-3 paragraphs)
33
+ ## Summary
34
+ (Summarize what was done in 2-3 paragraphs, based on git diff + conversation context)
88
35
 
89
36
  ## Deliverable Files
90
- - (list all component files)
91
-
92
- ## Component Structure
93
- (component tree overview)
94
-
95
- ## Mock Data Notes
96
- Current mock data used:
97
- ```typescript
98
- // Document all mock data and their locations
99
- // Include file name and line numbers
100
- ```
101
-
102
- ## Logic for Frontend to Implement
103
- 1. Replace mock login handler with real API call (file:line)
104
- 2. Add error handling for network failures
105
- 3. Implement loading state management
106
- 4. ...
107
-
108
- ## Special Notes for Downstream
109
- - (interactions to preserve)
110
- - (SDK dependencies)
111
- - (form validation already implemented)
112
- ```
37
+ (List all files in the output directory with one-line descriptions)
113
38
 
114
- Write the generated HANDOFF.md to `<output_dir>/HANDOFF.md`.
39
+ ## Key Design Decisions
40
+ (Numbered list of important decisions and rationale)
115
41
 
116
- ### Step 4: Ask for Handoff Message (Optional)
42
+ ## Notes for Downstream
43
+ (Specific items the next role should pay attention to)
117
44
 
118
- Ask the user:
119
- > "Any additional notes for the downstream role? (Press Enter to skip)"
45
+ ## Contact
46
+ Questions? Reach out to {developer name}
47
+ ````
120
48
 
121
- If provided, append to the Special Notes section.
49
+ ## Step 4: Update CHANGELOG.md
122
50
 
123
- ### Step 5: Add to Pool
51
+ 1. If `CHANGELOG.md` doesn't exist in the output directory, create it with the standard header:
52
+ ````
53
+ # CHANGELOG - {directory name}
124
54
 
125
- Determine the target pool based on role:
126
- - `pm` -> `requirements`
127
- - `designer` -> `prototypes`
128
- - `frontend-impl` or `frontend` -> `implementations`
55
+ > 变更记录表,由 AI 自动维护(/trellis:handoff 时追加)。
129
56
 
130
- ```bash
131
- python3 ./.trellis/scripts/pool.py add <pool> <slug> "<title>" <output_dir>
132
- ```
57
+ | 日期 | 作者 | 类型 | 摘要 | 关联文件 |
58
+ |------|------|------|------|----------|
59
+ ````
60
+ 2. Append one row to the changelog table:
61
+ - Date: current datetime (YYYY-MM-DD HH:mm)
62
+ - Author: developer name from .developer
63
+ - Type: Infer from git diff (新增/修改/删除/重构)
64
+ - Summary: One sentence (max 80 chars) summarizing the changes
65
+ - Files: Key files changed
133
66
 
134
- ### Step 6: Report
67
+ ## Step 5: User Review
135
68
 
136
- Output a summary:
137
- ```
138
- Handoff complete!
139
- - Deliverable: <slug>
140
- - Pool: <pool>
141
- - HANDOFF.md: <output_dir>/HANDOFF.md
142
- - Status: available
69
+ 1. Show the generated HANDOFF.md content to the user
70
+ 2. Show the new CHANGELOG.md entry
71
+ 3. Ask: "Please review the handoff document. Reply with changes or confirm to finalize."
72
+ 4. If the user requests changes, modify and re-show
73
+ 5. Once confirmed, remind:
74
+ - `git add {output_dir}/` to stage changes
75
+ - `git commit` and `git push`
76
+ - Notify the downstream developer offline
143
77
 
144
- Next: The downstream role can pick this up with:
145
- /trellis:pick-task <pool> <slug>
146
- ```
78
+ ## Constraints
147
79
 
148
- Remind the user to commit and push so the downstream role can access the deliverables.
80
+ - **NEVER** write files outside the bound output directory
81
+ - HANDOFF.md and CHANGELOG.md must be placed at the **root** of the output directory
82
+ - If `--message` argument is provided, include it in the HANDOFF.md Notes section
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ PreToolUse Hook: Enforce output directory constraint.
5
+
6
+ Intercepts Edit/Write/MultiEdit tool calls and verifies the target
7
+ file path is within the developer's allowed output directory.
8
+
9
+ Non-collaboration projects (no roles.json) are unaffected.
10
+ Fail-open: any infrastructure error results in allow.
11
+ """
12
+
13
+ # IMPORTANT: Suppress all warnings FIRST
14
+ import warnings
15
+ warnings.filterwarnings("ignore")
16
+
17
+ import json
18
+ import os
19
+ import sys
20
+ from pathlib import Path
21
+
22
+ # IMPORTANT: Force stdout to use UTF-8 on Windows
23
+ if sys.platform == "win32":
24
+ import io as _io
25
+ if hasattr(sys.stdout, "reconfigure"):
26
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr]
27
+ elif hasattr(sys.stdout, "detach"):
28
+ sys.stdout = _io.TextIOWrapper(sys.stdout.detach(), encoding="utf-8", errors="replace") # type: ignore[union-attr]
29
+
30
+
31
+ def _allow():
32
+ """Output allow decision and exit."""
33
+ print(json.dumps({"decision": "allow"}))
34
+
35
+
36
+ def _block(reason: str):
37
+ """Output block decision with reason and exit."""
38
+ print(json.dumps({"decision": "block", "reason": reason}))
39
+
40
+
41
+ def main():
42
+ # Read hook event from stdin
43
+ try:
44
+ event = json.loads(sys.stdin.read())
45
+ except (json.JSONDecodeError, EOFError, OSError):
46
+ _allow()
47
+ return
48
+
49
+ tool_name = event.get("tool_name", "")
50
+ tool_input = event.get("tool_input", {})
51
+
52
+ # Only intercept file-writing tools
53
+ if tool_name not in ("Edit", "Write", "MultiEdit"):
54
+ _allow()
55
+ return
56
+
57
+ # Get file path from tool input
58
+ file_path = tool_input.get("file_path", "")
59
+ if not file_path:
60
+ _allow()
61
+ return
62
+
63
+ # Resolve role constraint via .trellis/scripts/common/roles.py
64
+ try:
65
+ project_dir = Path(os.environ.get("CLAUDE_PROJECT_DIR", ".")).resolve()
66
+ scripts_parent = project_dir / ".trellis"
67
+ if scripts_parent.is_dir():
68
+ sys.path.insert(0, str(scripts_parent))
69
+
70
+ from scripts.common.roles import resolve_role_constraint
71
+ role, output_dir = resolve_role_constraint(project_dir)
72
+ except Exception:
73
+ # Fail-open: can't resolve constraint -> allow
74
+ _allow()
75
+ return
76
+
77
+ # No constraint = non-collaboration project, allow everything
78
+ if not role or not output_dir:
79
+ _allow()
80
+ return
81
+
82
+ # Normalize paths for comparison
83
+ try:
84
+ resolved_file = Path(file_path).resolve()
85
+ resolved_output = (project_dir / output_dir).resolve()
86
+ resolved_trellis = (project_dir / ".trellis").resolve()
87
+ except Exception:
88
+ _allow()
89
+ return
90
+
91
+ # Allow writes to .trellis/ (workflow files are always writable)
92
+ try:
93
+ resolved_file.relative_to(resolved_trellis)
94
+ _allow()
95
+ return
96
+ except ValueError:
97
+ pass
98
+
99
+ # Check if file is within the allowed output directory
100
+ try:
101
+ resolved_file.relative_to(resolved_output)
102
+ _allow()
103
+ return
104
+ except ValueError:
105
+ pass
106
+
107
+ # Block: file is outside allowed directory
108
+ _block(
109
+ f"Role '{role}' can only write to '{output_dir}' and '.trellis/'. "
110
+ f"Target file '{file_path}' is outside the allowed directory."
111
+ )
112
+
113
+
114
+ if __name__ == "__main__":
115
+ main()
@@ -3,7 +3,6 @@
3
3
  """
4
4
  Session Start Hook - Inject structured context
5
5
  """
6
- from __future__ import annotations
7
6
 
8
7
  # IMPORTANT: Suppress all warnings FIRST
9
8
  import warnings
@@ -66,167 +65,96 @@ def run_script(script_path: Path) -> str:
66
65
  return "No context available"
67
66
 
68
67
 
69
- def detect_role(trellis_dir: Path) -> str | None:
70
- """Detect role from developer name ({role}-{name} convention)."""
71
- dev_file = trellis_dir / ".developer"
72
- if not dev_file.is_file():
73
- return None
74
- try:
75
- content = dev_file.read_text(encoding="utf-8")
76
- for line in content.splitlines():
77
- if line.startswith("name="):
78
- name = line.split("=", 1)[1].strip()
79
- # Parse role from {role}-{name} pattern
80
- if "-" in name:
81
- role = name.split("-", 1)[0]
82
- if role in ("pm", "designer", "frontend", "frontend-impl"):
83
- return role
84
- except (OSError, IOError):
85
- pass
86
- return None
87
-
88
-
89
- def inject_upstream_context(trellis_dir: Path, project_dir: Path, output: StringIO) -> None:
90
- """Inject upstream context when source.json exists in current task."""
91
- current_task_file = trellis_dir / ".current-task"
92
- if not current_task_file.is_file():
93
- return
68
+ def inject_role_context(project_dir: Path, output):
69
+ """Inject role context and upstream handoff/changelog if in collaboration mode.
94
70
 
71
+ Fail-safe: any error is silently caught so it never breaks session start.
72
+ """
95
73
  try:
96
- task_rel = current_task_file.read_text(encoding="utf-8").strip()
97
- except (OSError, IOError):
98
- return
99
-
100
- if not task_rel:
101
- return
102
-
103
- task_dir = project_dir / task_rel
104
- source_file = task_dir / "source.json"
105
- if not source_file.is_file():
106
- return
74
+ trellis_dir = project_dir / ".trellis"
75
+ if trellis_dir.is_dir():
76
+ sys.path.insert(0, str(trellis_dir))
77
+
78
+ from scripts.common.roles import (
79
+ resolve_role_constraint,
80
+ load_roles_json,
81
+ get_upstream_dirs,
82
+ )
83
+ except ImportError:
84
+ return # roles module not available, skip silently
107
85
 
108
86
  try:
109
- source = json.loads(source_file.read_text(encoding="utf-8"))
110
- except (json.JSONDecodeError, OSError):
111
- return
112
-
113
- based_on = source.get("based_on")
114
- if not based_on:
115
- return
116
-
117
- output.write("<upstream-context>\n")
118
- output.write("## Based on Upstream Deliverable\n")
119
- output.write(f"- Type: {based_on.get('type', '?')}\n")
120
- output.write(f"- ID: {based_on.get('id', '?')}\n")
121
- output.write(f"- Path: {based_on.get('path', '?')}\n\n")
122
-
123
- # Read HANDOFF document
124
- handoff_path = project_dir / based_on.get("handoff_doc", "")
125
- if handoff_path.is_file():
126
- output.write("### Handoff Document\n")
127
- handoff_content = read_file(handoff_path)
128
- if len(handoff_content) > 3000:
129
- handoff_content = handoff_content[:3000] + "\n... (truncated)"
130
- output.write(handoff_content)
131
- output.write("\n\n")
132
-
133
- # Read upstream deliverable files
134
- source_path = project_dir / based_on.get("path", "")
135
- if source_path.is_dir():
136
- output.write("### Upstream Deliverable Files\n")
137
- total_size = 0
138
- text_exts = {".md", ".txt", ".tsx", ".ts", ".jsx", ".js", ".vue", ".css", ".json", ".yaml", ".yml", ".html"}
139
- for fpath in sorted(source_path.rglob("*")):
140
- if not fpath.is_file():
141
- continue
142
- if fpath.name == "HANDOFF.md":
143
- continue
144
- if fpath.suffix not in text_exts:
87
+ role, output_dir = resolve_role_constraint(project_dir)
88
+ if not role:
89
+ return # Not in collaboration mode
90
+
91
+ # --- Role context ---
92
+ output.write("<role-context>\n")
93
+ output.write(f"## Current Role: {role}\n")
94
+ output.write(f"## Output Directory: {output_dir}\n")
95
+ output.write("\n")
96
+
97
+ # Role spec files
98
+ role_dir_map = {"pm": "pm", "designer": "designer", "frontend": "frontend-impl"}
99
+ spec_dir = project_dir / ".trellis" / "spec" / "roles"
100
+ role_spec_dir = spec_dir / role_dir_map.get(role, role)
101
+ if role_spec_dir.is_dir():
102
+ for spec_file in sorted(role_spec_dir.glob("*.md")):
103
+ if spec_file.is_file():
104
+ try:
105
+ rel = spec_file.relative_to(project_dir)
106
+ except ValueError:
107
+ rel = spec_file.name
108
+ output.write(f"### {rel}\n")
109
+ content = spec_file.read_text(encoding="utf-8")
110
+ output.write(content[:3000])
111
+ if len(content) > 3000:
112
+ output.write("\n... (truncated)")
113
+ output.write("\n\n")
114
+
115
+ output.write("</role-context>\n\n")
116
+
117
+ # --- Upstream context ---
118
+ roles_json = load_roles_json(project_dir)
119
+ if not roles_json:
120
+ return
121
+
122
+ upstream_dirs = get_upstream_dirs(role, roles_json)
123
+ if not upstream_dirs:
124
+ return
125
+
126
+ output.write("<upstream-context>\n")
127
+ for upstream_dir in upstream_dirs:
128
+ upstream_path = project_dir / upstream_dir
129
+ if not upstream_path.is_dir():
145
130
  continue
146
- if total_size >= 50000:
147
- output.write("\n(total size cap reached, remaining files omitted)\n")
148
- break
149
- try:
150
- content = fpath.read_text(encoding="utf-8")
151
- except (OSError, UnicodeDecodeError):
152
- continue
153
- rel = fpath.relative_to(source_path)
154
- output.write(f"\n#### {rel}\n")
155
- ext = fpath.suffix[1:] if fpath.suffix else ""
156
- if len(content) > 3000:
157
- content = content[:3000] + "\n... (truncated)"
158
- output.write(f"```{ext}\n{content}\n```\n")
159
- total_size += len(content)
160
-
161
- output.write("</upstream-context>\n\n")
162
-
163
-
164
- def inject_pool_summary(trellis_dir: Path, output: StringIO) -> None:
165
- """Inject available tasks summary from pool files."""
166
- pool_dir = trellis_dir / "pool"
167
- if not pool_dir.is_dir():
168
- return
169
-
170
- pool_files = sorted(pool_dir.glob("*.json"))
171
- if not pool_files:
172
- return
173
-
174
- has_available = False
175
- pool_output = StringIO()
176
-
177
- for pool_file in pool_files:
178
- try:
179
- data = json.loads(pool_file.read_text(encoding="utf-8"))
180
- except (json.JSONDecodeError, OSError):
181
- continue
182
-
183
- available = [
184
- i for i in data.get("available", [])
185
- if i.get("status") == "available"
186
- ]
187
- if not available:
188
- continue
189
-
190
- has_available = True
191
- pool_name = pool_file.stem
192
- pool_output.write(f"## Available in {pool_name} ({len(available)})\n")
193
- for item in available:
194
- completed_by = item.get("completed_by", "?")
195
- completed_at = item.get("completed_at", "?")
196
- pool_output.write(
197
- f"- {item.get('id', '?')}: {item.get('title', '?')} "
198
- f"(by {completed_by}, {completed_at})\n"
199
- )
200
- pool_output.write("\n")
201
-
202
- if has_available:
203
- output.write("<available-tasks>\n")
204
- output.write(pool_output.getvalue())
205
- output.write("Use `/trellis:pick-task <pool> <id>` to start working on an available item.\n")
206
- output.write("</available-tasks>\n\n")
207
-
208
-
209
- def inject_role_guidelines(trellis_dir: Path, role: str | None, output: StringIO) -> None:
210
- """Inject role-specific guidelines if role is detected."""
211
- if not role:
212
- return
213
-
214
- role_dir_map = {
215
- "pm": "pm",
216
- "designer": "designer",
217
- "frontend": "frontend-impl",
218
- "frontend-impl": "frontend-impl",
219
- }
220
-
221
- role_dir = role_dir_map.get(role)
222
- if not role_dir:
223
- return
224
-
225
- role_index = trellis_dir / "spec" / "roles" / role_dir / "index.md"
226
- if role_index.is_file():
227
- output.write(f"## Role: {role}\n")
228
- output.write(read_file(role_index))
229
- output.write("\n\n")
131
+ # HANDOFF files
132
+ for handoff in sorted(upstream_path.rglob("HANDOFF.md")):
133
+ try:
134
+ rel = handoff.relative_to(project_dir)
135
+ except ValueError:
136
+ rel = handoff.name
137
+ output.write(f"### {rel}\n")
138
+ content = handoff.read_text(encoding="utf-8")
139
+ output.write(content[:3000])
140
+ if len(content) > 3000:
141
+ output.write("\n... (truncated)")
142
+ output.write("\n\n")
143
+ # CHANGELOG files
144
+ for changelog in sorted(upstream_path.rglob("CHANGELOG.md")):
145
+ try:
146
+ rel = changelog.relative_to(project_dir)
147
+ except ValueError:
148
+ rel = changelog.name
149
+ output.write(f"### {rel}\n")
150
+ content = changelog.read_text(encoding="utf-8")
151
+ output.write(content[:3000])
152
+ if len(content) > 3000:
153
+ output.write("\n... (truncated)")
154
+ output.write("\n\n")
155
+ output.write("</upstream-context>\n\n")
156
+ except Exception:
157
+ pass # Fail-safe: never break session start
230
158
 
231
159
 
232
160
  def main():
@@ -258,10 +186,6 @@ Read and follow all instructions below carefully.
258
186
 
259
187
  output.write("<guidelines>\n")
260
188
 
261
- # Detect role and inject role-specific guidelines
262
- role = detect_role(trellis_dir)
263
- inject_role_guidelines(trellis_dir, role, output)
264
-
265
189
  output.write("## Frontend\n")
266
190
  frontend_index = read_file(
267
191
  trellis_dir / "spec" / "frontend" / "index.md", "Not configured"
@@ -284,11 +208,8 @@ Read and follow all instructions below carefully.
284
208
 
285
209
  output.write("\n</guidelines>\n\n")
286
210
 
287
- # Inject upstream context (from source.json in current task)
288
- inject_upstream_context(trellis_dir, project_dir, output)
289
-
290
- # Inject pool summary (available tasks)
291
- inject_pool_summary(trellis_dir, output)
211
+ # Inject role context and upstream handoff/changelog (collaboration mode)
212
+ inject_role_context(project_dir, output)
292
213
 
293
214
  output.write("<instructions>\n")
294
215
  start_md = read_file(
@@ -22,6 +22,16 @@
22
22
  "timeout": 30
23
23
  }
24
24
  ]
25
+ },
26
+ {
27
+ "matcher": "Edit|Write|MultiEdit",
28
+ "hooks": [
29
+ {
30
+ "type": "command",
31
+ "command": "{{PYTHON_CMD}} .claude/hooks/enforce-output-dir.py",
32
+ "timeout": 10
33
+ }
34
+ ]
25
35
  }
26
36
  ],
27
37
  "SubagentStop": [