@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.
- package/dist/cli/index.js +1 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +98 -5
- package/dist/commands/init.js.map +1 -1
- package/dist/configurators/workflow.d.ts.map +1 -1
- package/dist/configurators/workflow.js +8 -58
- package/dist/configurators/workflow.js.map +1 -1
- package/dist/constants/paths.d.ts +0 -17
- package/dist/constants/paths.d.ts.map +1 -1
- package/dist/constants/paths.js +0 -19
- package/dist/constants/paths.js.map +1 -1
- package/dist/templates/claude/commands/trellis/handoff.md +56 -122
- package/dist/templates/claude/hooks/enforce-output-dir.py +115 -0
- package/dist/templates/claude/hooks/session-start.py +87 -166
- package/dist/templates/claude/settings.json +10 -0
- package/dist/templates/iflow/hooks/session-start.py +0 -171
- package/dist/templates/markdown/index.d.ts +0 -9
- package/dist/templates/markdown/index.d.ts.map +1 -1
- package/dist/templates/markdown/index.js +0 -10
- package/dist/templates/markdown/index.js.map +1 -1
- package/dist/templates/trellis/index.d.ts +9 -1
- package/dist/templates/trellis/index.d.ts.map +1 -1
- package/dist/templates/trellis/index.js +17 -2
- package/dist/templates/trellis/index.js.map +1 -1
- package/dist/templates/trellis/scripts/common/__init__.py +11 -0
- package/dist/templates/trellis/scripts/common/paths.py +1 -49
- package/dist/templates/trellis/scripts/common/roles.py +252 -0
- package/dist/templates/trellis/spec/roles/designer/index.md +49 -0
- package/dist/templates/trellis/spec/roles/frontend-impl/index.md +52 -0
- package/dist/templates/trellis/spec/roles/pm/index.md +44 -0
- package/dist/utils/template-hash.d.ts.map +1 -1
- package/dist/utils/template-hash.js +2 -0
- package/dist/utils/template-hash.js.map +1 -1
- package/package.json +2 -2
- package/dist/templates/claude/commands/trellis/pick-task.md +0 -145
- package/dist/templates/iflow/commands/trellis/handoff.md +0 -148
- package/dist/templates/iflow/commands/trellis/pick-task.md +0 -145
- package/dist/templates/markdown/spec/roles/designer/index.md.txt +0 -57
- package/dist/templates/markdown/spec/roles/designer/mock-data-standards.md.txt +0 -63
- package/dist/templates/markdown/spec/roles/designer/prototype-guidelines.md.txt +0 -49
- package/dist/templates/markdown/spec/roles/frontend-impl/api-integration.md.txt +0 -63
- package/dist/templates/markdown/spec/roles/frontend-impl/index.md.txt +0 -57
- package/dist/templates/markdown/spec/roles/frontend-impl/prototype-to-production.md.txt +0 -57
- package/dist/templates/markdown/spec/roles/pm/index.md.txt +0 -45
- package/dist/templates/markdown/spec/roles/pm/prd-template.md.txt +0 -64
- package/dist/templates/markdown/spec/roles/pm/requirement-checklist.md.txt +0 -43
- 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
|
-
|
|
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
|
-
|
|
39
|
-
> "The output directory `<output_dir>` appears empty. Would you like to continue anyway?"
|
|
7
|
+
## Step 1: Identify Context
|
|
40
8
|
|
|
41
|
-
|
|
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
|
-
|
|
14
|
+
## Step 2: Check Deliverables
|
|
44
15
|
|
|
45
|
-
|
|
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
|
-
|
|
48
|
-
# {Title} - Requirements Handoff
|
|
20
|
+
## Step 3: Generate HANDOFF.md
|
|
49
21
|
|
|
50
|
-
|
|
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
|
-
|
|
72
|
-
-
|
|
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
|
-
- **
|
|
82
|
-
- **
|
|
83
|
-
- **
|
|
84
|
-
- **Completed
|
|
28
|
+
- **Role**: {current role}
|
|
29
|
+
- **Developer**: {developer name}
|
|
30
|
+
- **Output Directory**: {bound directory}
|
|
31
|
+
- **Completed**: {current timestamp}
|
|
85
32
|
|
|
86
|
-
##
|
|
87
|
-
(
|
|
33
|
+
## Summary
|
|
34
|
+
(Summarize what was done in 2-3 paragraphs, based on git diff + conversation context)
|
|
88
35
|
|
|
89
36
|
## Deliverable Files
|
|
90
|
-
|
|
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
|
-
|
|
39
|
+
## Key Design Decisions
|
|
40
|
+
(Numbered list of important decisions and rationale)
|
|
115
41
|
|
|
116
|
-
|
|
42
|
+
## Notes for Downstream
|
|
43
|
+
(Specific items the next role should pay attention to)
|
|
117
44
|
|
|
118
|
-
|
|
119
|
-
|
|
45
|
+
## Contact
|
|
46
|
+
Questions? Reach out to {developer name}
|
|
47
|
+
````
|
|
120
48
|
|
|
121
|
-
|
|
49
|
+
## Step 4: Update CHANGELOG.md
|
|
122
50
|
|
|
123
|
-
|
|
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
|
-
|
|
126
|
-
- `pm` -> `requirements`
|
|
127
|
-
- `designer` -> `prototypes`
|
|
128
|
-
- `frontend-impl` or `frontend` -> `implementations`
|
|
55
|
+
> 变更记录表,由 AI 自动维护(/trellis:handoff 时追加)。
|
|
129
56
|
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
67
|
+
## Step 5: User Review
|
|
135
68
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
-
|
|
142
|
-
-
|
|
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
|
-
|
|
145
|
-
/trellis:pick-task <pool> <slug>
|
|
146
|
-
```
|
|
78
|
+
## Constraints
|
|
147
79
|
|
|
148
|
-
|
|
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
|
|
70
|
-
"""
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
|
288
|
-
|
|
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": [
|