@mindfoldhq/trellis 0.3.4 → 0.3.6

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 (57) hide show
  1. package/README.md +6 -4
  2. package/dist/cli/index.js +1 -0
  3. package/dist/cli/index.js.map +1 -1
  4. package/dist/commands/init.d.ts +1 -0
  5. package/dist/commands/init.d.ts.map +1 -1
  6. package/dist/commands/init.js +243 -49
  7. package/dist/commands/init.js.map +1 -1
  8. package/dist/commands/update.d.ts.map +1 -1
  9. package/dist/commands/update.js +3 -0
  10. package/dist/commands/update.js.map +1 -1
  11. package/dist/migrations/manifests/0.3.4.json +1 -1
  12. package/dist/migrations/manifests/0.3.5.json +9 -0
  13. package/dist/migrations/manifests/0.3.6.json +9 -0
  14. package/dist/templates/claude/commands/trellis/brainstorm.md +13 -0
  15. package/dist/templates/claude/commands/trellis/record-session.md +4 -1
  16. package/dist/templates/claude/commands/trellis/start.md +4 -0
  17. package/dist/templates/claude/hooks/inject-subagent-context.py +1 -1
  18. package/dist/templates/claude/settings.json +10 -0
  19. package/dist/templates/codex/skills/brainstorm/SKILL.md +13 -0
  20. package/dist/templates/codex/skills/record-session/SKILL.md +4 -1
  21. package/dist/templates/codex/skills/start/SKILL.md +4 -0
  22. package/dist/templates/cursor/commands/trellis-brainstorm.md +13 -0
  23. package/dist/templates/cursor/commands/trellis-record-session.md +4 -1
  24. package/dist/templates/cursor/commands/trellis-start.md +4 -0
  25. package/dist/templates/gemini/commands/trellis/brainstorm.toml +15 -0
  26. package/dist/templates/gemini/commands/trellis/record-session.toml +4 -1
  27. package/dist/templates/gemini/commands/trellis/start.toml +4 -0
  28. package/dist/templates/iflow/commands/trellis/brainstorm.md +13 -0
  29. package/dist/templates/iflow/commands/trellis/record-session.md +4 -1
  30. package/dist/templates/iflow/commands/trellis/start.md +4 -0
  31. package/dist/templates/iflow/hooks/inject-subagent-context.py +1 -1
  32. package/dist/templates/kilo/workflows/brainstorm.md +13 -0
  33. package/dist/templates/kilo/workflows/record-session.md +4 -1
  34. package/dist/templates/kilo/workflows/start.md +4 -0
  35. package/dist/templates/kiro/skills/brainstorm/SKILL.md +13 -0
  36. package/dist/templates/kiro/skills/record-session/SKILL.md +4 -1
  37. package/dist/templates/kiro/skills/start/SKILL.md +4 -0
  38. package/dist/templates/markdown/spec/backend/directory-structure.md +292 -0
  39. package/dist/templates/markdown/spec/backend/script-conventions.md +220 -38
  40. package/dist/templates/opencode/commands/trellis/brainstorm.md +13 -0
  41. package/dist/templates/opencode/commands/trellis/record-session.md +4 -1
  42. package/dist/templates/opencode/commands/trellis/start.md +4 -0
  43. package/dist/templates/qoder/skills/brainstorm/SKILL.md +13 -0
  44. package/dist/templates/qoder/skills/record-session/SKILL.md +4 -1
  45. package/dist/templates/qoder/skills/start/SKILL.md +4 -0
  46. package/dist/templates/trellis/config.yaml +18 -0
  47. package/dist/templates/trellis/scripts/common/config.py +20 -0
  48. package/dist/templates/trellis/scripts/common/git_context.py +160 -12
  49. package/dist/templates/trellis/scripts/common/task_queue.py +4 -0
  50. package/dist/templates/trellis/scripts/common/worktree.py +78 -11
  51. package/dist/templates/trellis/scripts/create_bootstrap.py +3 -0
  52. package/dist/templates/trellis/scripts/task.py +312 -17
  53. package/dist/utils/template-fetcher.d.ts +57 -4
  54. package/dist/utils/template-fetcher.d.ts.map +1 -1
  55. package/dist/utils/template-fetcher.js +179 -10
  56. package/dist/utils/template-fetcher.js.map +1 -1
  57. package/package.json +7 -7
@@ -0,0 +1,292 @@
1
+ # Directory Structure
2
+
3
+ > How backend/CLI code is organized in this project.
4
+
5
+ ---
6
+
7
+ ## Overview
8
+
9
+ This project is a **TypeScript CLI tool** using ES modules. The source code follows a **dogfooding architecture** - Trellis uses its own configuration files (`.cursor/`, `.claude/`, `.trellis/`) as templates for new projects.
10
+
11
+ ---
12
+
13
+ ## Directory Layout
14
+
15
+ ```
16
+ src/
17
+ ├── cli/ # CLI entry point and argument parsing
18
+ │ └── index.ts # Main CLI entry (Commander.js setup)
19
+ ├── commands/ # Command implementations
20
+ │ └── init.ts # Each command in its own file
21
+ ├── configurators/ # Configuration generators
22
+ │ ├── index.ts # Platform registry (PLATFORM_FUNCTIONS, derived helpers)
23
+ │ ├── shared.ts # Shared utilities (resolvePlaceholders)
24
+ │ ├── claude.ts # Claude Code configurator
25
+ │ ├── cursor.ts # Cursor configurator
26
+ │ ├── iflow.ts # iFlow CLI configurator
27
+ │ ├── opencode.ts # OpenCode configurator
28
+ │ └── workflow.ts # Creates .trellis/ structure
29
+ ├── constants/ # Shared constants and paths
30
+ │ └── paths.ts # Path constants (centralized)
31
+ ├── templates/ # Template utilities and generic templates
32
+ │ ├── markdown/ # Generic markdown templates
33
+ │ │ ├── spec/ # Spec templates (*.md.txt)
34
+ │ │ ├── init-agent.md # Project root file template
35
+ │ │ ├── agents.md # Project root file template
36
+ │ │ ├── worktree.yaml.txt # Generic worktree config
37
+ │ │ └── index.ts # Template exports
38
+ │ └── extract.ts # Template extraction utilities
39
+ ├── types/ # TypeScript type definitions
40
+ │ └── ai-tools.ts # AI tool types and registry
41
+ ├── utils/ # Shared utility functions
42
+ │ ├── compare-versions.ts # Semver comparison with prerelease support
43
+ │ ├── file-writer.ts # File writing with conflict handling
44
+ │ ├── project-detector.ts # Project type detection
45
+ │ ├── template-fetcher.ts # Remote template download from GitHub
46
+ │ └── template-hash.ts # Template hash tracking for update detection
47
+ └── index.ts # Package entry point (exports public API)
48
+ ```
49
+
50
+ ### Dogfooding Directories (Project Root)
51
+
52
+ These directories are copied to `dist/` during build and used as templates:
53
+
54
+ ```
55
+ .cursor/ # Cursor configuration (dogfooded)
56
+ ├── commands/ # Slash commands for Cursor
57
+ │ ├── start.md
58
+ │ ├── finish-work.md
59
+ │ └── ...
60
+
61
+ .claude/ # Claude Code configuration (dogfooded)
62
+ ├── commands/ # Slash commands
63
+ ├── agents/ # Multi-agent pipeline agents
64
+ ├── hooks/ # Context injection hooks
65
+ └── settings.json # Hook configuration
66
+
67
+ .trellis/ # Trellis workflow (partially dogfooded)
68
+ ├── scripts/ # Python scripts (dogfooded)
69
+ │ ├── common/ # Shared utilities (paths.py, developer.py, etc.)
70
+ │ ├── multi_agent/ # Pipeline scripts (start.py, status.py, etc.)
71
+ │ ├── hooks/ # Lifecycle hook scripts (project-specific, NOT dogfooded)
72
+ │ └── *.py # Main scripts (task.py, get_context.py, etc.)
73
+ ├── workspace/ # Developer progress tracking
74
+ │ └── index.md # Index template (dogfooded)
75
+ ├── spec/ # Project guidelines (NOT dogfooded)
76
+ │ ├── backend/ # Backend development docs
77
+ │ ├── frontend/ # Frontend development docs
78
+ │ └── guides/ # Thinking guides
79
+ ├── workflow.md # Workflow documentation (dogfooded)
80
+ ├── worktree.yaml # Worktree config (Trellis-specific)
81
+ └── .gitignore # Git ignore rules (dogfooded)
82
+ ```
83
+
84
+ ---
85
+
86
+ ## Dogfooding Architecture
87
+
88
+ ### What is Dogfooded
89
+
90
+ Files that are copied directly from Trellis project to user projects:
91
+
92
+ | Source | Destination | Description |
93
+ |--------|-------------|-------------|
94
+ | `.cursor/` | `.cursor/` | Entire directory copied |
95
+ | `.claude/` | `.claude/` | Entire directory copied |
96
+ | `.trellis/scripts/` | `.trellis/scripts/` | All scripts copied |
97
+ | `.trellis/workflow.md` | `.trellis/workflow.md` | Direct copy |
98
+ | `.trellis/.gitignore` | `.trellis/.gitignore` | Direct copy |
99
+ | `.trellis/workspace/index.md` | `.trellis/workspace/index.md` | Direct copy |
100
+
101
+ ### What is NOT Dogfooded
102
+
103
+ Files that use generic templates (in `src/templates/`):
104
+
105
+ | Template Source | Destination | Reason |
106
+ |----------------|-------------|--------|
107
+ | `src/templates/markdown/spec/**/*.md.txt` | `.trellis/spec/**/*.md` | User fills with project-specific content |
108
+ | `src/templates/markdown/worktree.yaml.txt` | `.trellis/worktree.yaml` | Language-agnostic template |
109
+ | `src/templates/markdown/init-agent.md` | `init-agent.md` | Project root file |
110
+ | `src/templates/markdown/agents.md` | `AGENTS.md` | Project root file |
111
+
112
+ ### Build Process
113
+
114
+ ```bash
115
+ # scripts/copy-templates.js copies dogfooding sources to dist/
116
+ pnpm build
117
+
118
+ # Result:
119
+ dist/
120
+ ├── .cursor/ # From project root .cursor/
121
+ ├── .claude/ # From project root .claude/
122
+ ├── .trellis/ # From project root .trellis/ (filtered)
123
+ │ ├── scripts/ # All scripts
124
+ │ ├── workspace/
125
+ │ │ └── index.md # Only index.md, no developer subdirs
126
+ │ ├── workflow.md
127
+ │ ├── worktree.yaml
128
+ │ └── .gitignore
129
+ └── templates/ # From src/templates/ (no .ts files)
130
+ └── markdown/
131
+ └── spec/ # Generic templates
132
+ ```
133
+
134
+ ---
135
+
136
+ ## Module Organization
137
+
138
+ ### Layer Responsibilities
139
+
140
+ | Layer | Directory | Responsibility |
141
+ |-------|-----------|----------------|
142
+ | CLI | `cli/` | Parse arguments, display help, call commands |
143
+ | Commands | `commands/` | Implement CLI commands, orchestrate actions |
144
+ | Configurators | `configurators/` | Copy/generate configuration for tools |
145
+ | Templates | `templates/` | Extract template content, provide utilities |
146
+ | Types | `types/` | TypeScript type definitions |
147
+ | Utils | `utils/` | Reusable utility functions |
148
+ | Constants | `constants/` | Shared constants (paths, names) |
149
+
150
+ ### Configurator Pattern
151
+
152
+ Configurators use `cpSync` for direct directory copy (dogfooding):
153
+
154
+ ```typescript
155
+ // configurators/cursor.ts
156
+ export async function configureCursor(cwd: string): Promise<void> {
157
+ const sourcePath = getCursorSourcePath(); // dist/.cursor/ or .cursor/
158
+ const destPath = path.join(cwd, ".cursor");
159
+ cpSync(sourcePath, destPath, { recursive: true });
160
+ }
161
+ ```
162
+
163
+ ### Template Extraction
164
+
165
+ `extract.ts` provides utilities for reading dogfooded files:
166
+
167
+ ```typescript
168
+ // Get path to .trellis/ (works in dev and production)
169
+ getTrellisSourcePath(): string
170
+
171
+ // Read file from .trellis/
172
+ readTrellisFile(relativePath: string): string
173
+
174
+ // Copy directory from .trellis/ with executable scripts
175
+ copyTrellisDir(srcRelativePath: string, destPath: string, options?: { executable?: boolean }): void
176
+ ```
177
+
178
+ ---
179
+
180
+ ## Naming Conventions
181
+
182
+ ### Files and Directories
183
+
184
+ | Convention | Example | Usage |
185
+ |------------|---------|-------|
186
+ | `kebab-case` | `file-writer.ts` | All TypeScript files |
187
+ | `kebab-case` | `multi-agent/` | All directories |
188
+ | `*.ts` | `init.ts` | TypeScript source files |
189
+ | `*.md.txt` | `index.md.txt` | Template files for markdown |
190
+ | `*.yaml.txt` | `worktree.yaml.txt` | Template files for yaml |
191
+
192
+ ### Why `.txt` Extension for Templates
193
+
194
+ Templates use `.txt` extension to:
195
+ - Prevent IDE markdown preview from rendering templates
196
+ - Make clear these are template sources, not actual docs
197
+ - Avoid confusion with actual markdown files
198
+
199
+ ---
200
+
201
+ ## DO / DON'T
202
+
203
+ ### DO
204
+
205
+ - Dogfood from project's own config files when possible
206
+ - Use `cpSync` for copying entire directories
207
+ - Keep generic templates in `src/templates/markdown/`
208
+ - Use `.md.txt` or `.yaml.txt` for template files
209
+ - Update dogfooding sources (`.cursor/`, `.claude/`, `.trellis/scripts/`) when making changes
210
+ - Always use `python3` explicitly when documenting script invocation (Windows compatibility)
211
+
212
+ ### DON'T
213
+
214
+ - Don't hardcode file lists - copy entire directories instead
215
+ - Don't duplicate content between templates and dogfooding sources
216
+ - Don't put project-specific content in generic templates
217
+ - Don't use dogfooding for spec/ (users fill these in)
218
+
219
+ ---
220
+
221
+ ## Design Decisions
222
+
223
+ ### Remote Template Download (giget)
224
+
225
+ **Context**: Need to download GitHub subdirectories for remote template support.
226
+
227
+ **Options Considered**:
228
+ 1. `degit` / `tiged` - Simple, but no programmatic API
229
+ 2. `giget` - TypeScript native, has programmatic API, used by Nuxt/UnJS
230
+ 3. Manual GitHub API - Too complex
231
+
232
+ **Decision**: Use `giget` because:
233
+ - TypeScript native with programmatic API
234
+ - Supports GitHub subdirectory: `gh:user/repo/path/to/subdir`
235
+ - Built-in caching for offline support
236
+ - Actively maintained by UnJS ecosystem
237
+
238
+ **Example**:
239
+ ```typescript
240
+ import { downloadTemplate } from "giget";
241
+
242
+ await downloadTemplate("gh:mindfold-ai/docs/marketplace/specs/electron-fullstack", {
243
+ dir: destDir,
244
+ preferOffline: true,
245
+ });
246
+ ```
247
+
248
+ ### Directory Conflict Strategy (skip/overwrite/append)
249
+
250
+ **Context**: When downloading remote templates, target directory may already exist.
251
+
252
+ **Decision**: Three strategies with `skip` as default:
253
+ - `skip` - Don't download if directory exists (safe default)
254
+ - `overwrite` - Delete existing, download fresh
255
+ - `append` - Only copy files that don't exist (merge)
256
+
257
+ **Why**: giget doesn't support append natively, so we:
258
+ 1. Download to temp directory
259
+ 2. Walk and copy missing files only
260
+ 3. Clean up temp directory
261
+
262
+ **Example**:
263
+ ```typescript
264
+ // append strategy implementation
265
+ const tempDir = path.join(os.tmpdir(), `trellis-template-${Date.now()}`);
266
+ await downloadTemplate(source, { dir: tempDir });
267
+ await copyMissing(tempDir, destDir); // Only copy non-existing files
268
+ await fs.promises.rm(tempDir, { recursive: true });
269
+ ```
270
+
271
+ ### Extensible Template Type Mapping
272
+
273
+ **Context**: Currently only `spec` templates, but future needs `skill`, `command`, `full` types.
274
+
275
+ **Decision**: Use type field + mapping table for extensibility:
276
+
277
+ ```typescript
278
+ const INSTALL_PATHS: Record<string, string> = {
279
+ spec: ".trellis/spec",
280
+ skill: ".claude/skills",
281
+ command: ".claude/commands",
282
+ full: ".", // Entire project root
283
+ };
284
+
285
+ // Usage: auto-detect install path from template type
286
+ const destDir = INSTALL_PATHS[template.type] || INSTALL_PATHS.spec;
287
+ ```
288
+
289
+ **Extensibility**: To add new template type:
290
+ 1. Add entry to `INSTALL_PATHS`
291
+ 2. Add templates to `index.json` with new type
292
+ 3. No code changes needed for download logic
@@ -23,8 +23,11 @@ All workflow scripts are written in **Python 3.10+** for cross-platform compatib
23
23
  │ ├── task_utils.py # Task helper functions
24
24
  │ ├── phase.py # Multi-agent phase tracking
25
25
  │ ├── registry.py # Agent registry management
26
- │ ├── worktree.py # Git worktree utilities
26
+ │ ├── config.py # Config reader (config.yaml, hooks)
27
+ │ ├── worktree.py # Git worktree utilities + YAML parser
27
28
  │ └── git_context.py # Git/session context
29
+ ├── hooks/ # Lifecycle hook scripts (project-specific)
30
+ │ └── linear_sync.py # Example: sync tasks to Linear
28
31
  ├── multi_agent/ # Multi-agent pipeline scripts
29
32
  │ ├── __init__.py
30
33
  │ ├── start.py # Start worktree agent
@@ -81,10 +84,19 @@ Task Management Script.
81
84
 
82
85
  Usage:
83
86
  python3 task.py create "<title>" [--slug <name>]
84
- python3 task.py list [--mine] [--status <status>]
87
+ python3 task.py init-context <dir> <dev_type>
88
+ python3 task.py add-context <dir> <file> <reason>
89
+ python3 task.py validate <dir>
90
+ python3 task.py list-context <dir>
85
91
  python3 task.py start <dir>
86
92
  python3 task.py finish
93
+ python3 task.py set-branch <dir> <branch>
94
+ python3 task.py set-base-branch <dir> <branch>
95
+ python3 task.py set-scope <dir> <scope>
96
+ python3 task.py create-pr [dir] [--dry-run]
87
97
  python3 task.py archive <task-name>
98
+ python3 task.py list [--mine] [--status <status>]
99
+ python3 task.py list-archive [YYYY-MM]
88
100
  """
89
101
 
90
102
  from __future__ import annotations
@@ -202,11 +214,13 @@ def run_command(
202
214
 
203
215
  ## Cross-Platform Compatibility
204
216
 
205
- ### CRITICAL: Windows stdout Encoding
217
+ ### CRITICAL: Windows stdio Encoding (stdout + stdin)
206
218
 
207
- On Windows, Python's stdout defaults to the system code page (e.g., GBK/CP936 in China, CP1252 in Western locales). This causes `UnicodeEncodeError` when printing non-ASCII characters.
219
+ On Windows, Python's stdout AND stdin default to the system code page (e.g., GBK/CP936 in China, CP1252 in Western locales). This causes:
220
+ - `UnicodeEncodeError` when **printing** non-ASCII characters (stdout)
221
+ - `UnicodeDecodeError` when **reading piped** UTF-8 content (stdin), e.g. Chinese text via `cat << EOF | python3 script.py`
208
222
 
209
- **The Problem Chain**:
223
+ **The Problem Chain (stdout)**:
210
224
 
211
225
  ```
212
226
  Windows code page = GBK (936)
@@ -220,60 +234,64 @@ json.dumps(ensure_ascii=False) → print()
220
234
  GBK cannot encode \ufffd → UnicodeEncodeError: 'gbk' codec can't encode character
221
235
  ```
222
236
 
223
- **Root Cause**: Even if you set `PYTHONIOENCODING` in subprocess calls, the **parent process's stdout** still uses the system code page. The error occurs when `print()` tries to write to stdout.
224
-
225
- ---
237
+ **The Problem Chain (stdin)**:
226
238
 
227
- #### GOOD: Use `sys.stdout.reconfigure()` (Python 3.7+)
239
+ ```
240
+ AI agent pipes UTF-8 content via heredoc: cat << 'EOF' | python3 add_session.py ...
241
+
242
+ Python stdin defaults to GBK encoding (PowerShell default code page)
243
+
244
+ sys.stdin.read() decodes bytes as GBK, not UTF-8
245
+
246
+ Chinese text garbled or UnicodeDecodeError
247
+ ```
228
248
 
229
- ```python
230
- import sys
249
+ **Root Cause**: Even if you set `PYTHONIOENCODING` in subprocess calls, the **parent process's stdio** still uses the system code page.
231
250
 
232
- # MUST be at the top of the script, before any print() calls
233
- if sys.platform == "win32":
234
- sys.stdout.reconfigure(encoding="utf-8", errors="replace")
235
- sys.stderr.reconfigure(encoding="utf-8", errors="replace")
236
- ```
251
+ ---
237
252
 
238
- **Why this works**: `reconfigure()` modifies the existing stream **in-place**, changing its encoding settings directly. This affects all subsequent writes to stdout.
253
+ #### GOOD: Centralize encoding fix in `common/__init__.py`
239
254
 
240
- **Best Practice**: Add this to `common/__init__.py` so all scripts that `from common import ...` automatically get the fix:
255
+ All stdio encoding is handled in one place. Scripts that `from common import ...` automatically get the fix:
241
256
 
242
257
  ```python
243
258
  # common/__init__.py
259
+ import io
244
260
  import sys
245
261
 
246
- if sys.platform == "win32":
247
- sys.stdout.reconfigure(encoding="utf-8", errors="replace")
248
- sys.stderr.reconfigure(encoding="utf-8", errors="replace")
262
+ def _configure_stream(stream):
263
+ """Configure a stream for UTF-8 encoding on Windows."""
264
+ if hasattr(stream, "reconfigure"):
265
+ stream.reconfigure(encoding="utf-8", errors="replace")
266
+ return stream
267
+ elif hasattr(stream, "detach"):
268
+ return io.TextIOWrapper(stream.detach(), encoding="utf-8", errors="replace")
269
+ return stream
249
270
 
250
- # ... rest of exports
271
+ if sys.platform == "win32":
272
+ sys.stdout = _configure_stream(sys.stdout)
273
+ sys.stderr = _configure_stream(sys.stderr)
274
+ sys.stdin = _configure_stream(sys.stdin) # Don't forget stdin!
251
275
  ```
252
276
 
253
277
  ---
254
278
 
255
- #### BAD: Do NOT use `io.TextIOWrapper`
279
+ #### DON'T: Inline encoding code in individual scripts
256
280
 
257
281
  ```python
258
- # BAD - This does NOT reliably fix the encoding issue!
282
+ # BAD - Duplicated in every script, easy to forget stdin
259
283
  import sys
260
- import io
261
-
262
284
  if sys.platform == "win32":
263
- sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
285
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
286
+ # Forgot stdin! Piped Chinese text will break.
264
287
  ```
265
288
 
266
- **Why this fails**:
289
+ **Why this is bad**:
290
+ 1. **Easy to forget streams**: stdout was fixed but stdin was missed in multiple scripts, causing real user bugs
291
+ 2. **Duplicated code**: Same logic copy-pasted across `add_session.py`, `git_context.py`, etc.
292
+ 3. **Inconsistent coverage**: Some scripts fix stdout only, others fix stdout+stderr, none fixed stdin
267
293
 
268
- 1. **Creates a new wrapper, doesn't fix the underlying issue**: `TextIOWrapper` wraps `sys.stdout.buffer`, but the original stdout object and its encoding settings may still interfere in some code paths.
269
-
270
- 2. **Loses original stdout properties**: The new wrapper may not preserve all attributes of the original `sys.stdout` (like `isatty()`, line buffering behavior).
271
-
272
- 3. **Race condition with buffering**: If any output was buffered before the replacement, it may still be encoded with the old encoding.
273
-
274
- 4. **Not idempotent**: Calling this multiple times creates nested wrappers, while `reconfigure()` is safe to call multiple times.
275
-
276
- **Real-world failure case**: Users reported that `io.TextIOWrapper` did not fix the `UnicodeEncodeError` on Windows, while `sys.stdout.reconfigure()` worked immediately.
294
+ **Real-world failure**: Users on Windows reported garbled Chinese text when using `cat << EOF | python3 add_session.py`. Root cause: stdin was never reconfigured to UTF-8.
277
295
 
278
296
  ---
279
297
 
@@ -281,7 +299,8 @@ if sys.platform == "win32":
281
299
 
282
300
  | Method | Works? | Reason |
283
301
  |--------|--------|--------|
284
- | `sys.stdout.reconfigure(encoding="utf-8")` | ✅ Yes | Modifies stream in-place |
302
+ | `common/__init__.py` centralized fix | ✅ Yes | All streams, all scripts, one place |
303
+ | `sys.stdout.reconfigure(encoding="utf-8")` | ⚠️ Partial | Only stdout; easy to forget stdin/stderr |
285
304
  | `io.TextIOWrapper(sys.stdout.buffer, ...)` | ❌ No | Creates wrapper, doesn't fix underlying encoding |
286
305
  | `PYTHONIOENCODING=utf-8` env var | ⚠️ Partial | Only works if set **before** Python starts |
287
306
 
@@ -320,6 +339,169 @@ path = ".trellis/scripts/task.py"
320
339
 
321
340
  ---
322
341
 
342
+ ## Task Lifecycle Hooks
343
+
344
+ ### Scope / Trigger
345
+
346
+ Task lifecycle events (`after_create`, `after_start`, `after_finish`, `after_archive`) execute user-defined shell commands configured in `config.yaml`.
347
+
348
+ ### Signatures
349
+
350
+ ```python
351
+ # config.py — read hook commands from config
352
+ def get_hooks(event: str, repo_root: Path | None = None) -> list[str]
353
+
354
+ # task.py — execute hooks (never blocks main operation)
355
+ def _run_hooks(event: str, task_json_path: Path, repo_root: Path) -> None
356
+ ```
357
+
358
+ ### Contracts
359
+
360
+ **Config format** (`config.yaml`):
361
+ ```yaml
362
+ hooks:
363
+ after_create:
364
+ - "python3 .trellis/scripts/hooks/my_hook.py create"
365
+ after_start:
366
+ - "python3 .trellis/scripts/hooks/my_hook.py start"
367
+ after_archive:
368
+ - "python3 .trellis/scripts/hooks/my_hook.py archive"
369
+ ```
370
+
371
+ **Environment variables passed to hooks**:
372
+
373
+ | Key | Type | Description |
374
+ |-----|------|-------------|
375
+ | `TASK_JSON_PATH` | Absolute path string | Path to the task's `task.json` |
376
+
377
+ - `cwd` is set to `repo_root`
378
+ - Hooks inherit the parent process environment + `TASK_JSON_PATH`
379
+
380
+ ### Subprocess Execution
381
+
382
+ ```python
383
+ import os
384
+ import subprocess
385
+
386
+ env = {**os.environ, "TASK_JSON_PATH": str(task_json_path)}
387
+
388
+ result = subprocess.run(
389
+ cmd,
390
+ shell=True,
391
+ cwd=repo_root,
392
+ env=env,
393
+ capture_output=True,
394
+ text=True,
395
+ encoding="utf-8", # REQUIRED: cross-platform
396
+ errors="replace", # REQUIRED: cross-platform
397
+ )
398
+ ```
399
+
400
+ ### Validation & Error Matrix
401
+
402
+ | Condition | Behavior |
403
+ |-----------|----------|
404
+ | No `hooks` key in config | No-op (empty list) |
405
+ | `hooks` is not a dict | No-op (empty list) |
406
+ | Event key missing | No-op (empty list) |
407
+ | Hook command exits non-zero | `[WARN]` to stderr, continues to next hook |
408
+ | Hook command throws exception | `[WARN]` to stderr, continues to next hook |
409
+ | `linearis` not installed | Hook fails with warning, task operation succeeds |
410
+
411
+ ### Wrong vs Correct
412
+
413
+ #### Wrong — blocking on hook failure
414
+ ```python
415
+ result = subprocess.run(cmd, shell=True, check=True) # Raises on failure!
416
+ ```
417
+
418
+ #### Correct — warn and continue
419
+ ```python
420
+ try:
421
+ result = subprocess.run(cmd, shell=True, ...)
422
+ if result.returncode != 0:
423
+ print(f"[WARN] Hook failed: {cmd}", file=sys.stderr)
424
+ except Exception as e:
425
+ print(f"[WARN] Hook error: {cmd} — {e}", file=sys.stderr)
426
+ ```
427
+
428
+ ### Hook Script Pattern
429
+
430
+ Hook scripts that need project-specific config (API keys, user IDs) should:
431
+ 1. Store config in a **gitignored** local file (e.g., `.trellis/hooks.local.json`)
432
+ 2. Read config at startup, fail with clear message if missing
433
+ 3. Keep the script itself committable (no hardcoded secrets)
434
+
435
+ ```python
436
+ # .trellis/scripts/hooks/my_hook.py — committable, no secrets
437
+ CONFIG = _load_config() # reads from .trellis/hooks.local.json (gitignored)
438
+ TEAM = CONFIG.get("linear", {}).get("team", "")
439
+ ```
440
+
441
+ ---
442
+
443
+ ## Auto-Commit Pattern
444
+
445
+ Scripts that modify `.trellis/` tracked files should auto-commit their changes to keep the workspace clean. Use a `--no-commit` flag for opt-out.
446
+
447
+ ### Convention: Auto-Commit After Mutation
448
+
449
+ ```python
450
+ def _auto_commit(scope: str, message: str, repo_root: Path) -> None:
451
+ """Stage and commit changes in a specific .trellis/ subdirectory."""
452
+ subprocess.run(["git", "add", "-A", scope], cwd=repo_root, capture_output=True)
453
+ # Check if there are staged changes
454
+ result = subprocess.run(
455
+ ["git", "diff", "--cached", "--quiet", "--", scope],
456
+ cwd=repo_root,
457
+ )
458
+ if result.returncode == 0:
459
+ print("[OK] No changes to commit.", file=sys.stderr)
460
+ return
461
+ commit_result = subprocess.run(
462
+ ["git", "commit", "-m", message],
463
+ cwd=repo_root, capture_output=True, text=True,
464
+ )
465
+ if commit_result.returncode == 0:
466
+ print(f"[OK] Auto-committed: {message}", file=sys.stderr)
467
+ else:
468
+ print(f"[WARN] Auto-commit failed: {commit_result.stderr.strip()}", file=sys.stderr)
469
+ ```
470
+
471
+ **Scripts using this pattern**:
472
+ - `add_session.py` — commits `.trellis/workspace` + `.trellis/tasks` after recording a session
473
+ - `task.py archive` — commits `.trellis/tasks` after archiving a task
474
+
475
+ **Always add `--no-commit` flag** for scripts that auto-commit, so users can opt out.
476
+
477
+ ---
478
+
479
+ ## CLI Mode Extension Pattern
480
+
481
+ ### Design Decision: `--mode` for Context-Dependent Output
482
+
483
+ When a script needs different output for different use cases, use `--mode` (not separate scripts or additional flags).
484
+
485
+ **Example**: `get_context.py` serves two modes:
486
+ - `--mode default` — full session context (DEVELOPER, GIT STATUS, RECENT COMMITS, CURRENT TASK, ACTIVE TASKS, MY TASKS, JOURNAL, PATHS)
487
+ - `--mode record` — focused output for record-session (MY ACTIVE TASKS first with emphasis, GIT STATUS, RECENT COMMITS, CURRENT TASK)
488
+
489
+ ```python
490
+ parser.add_argument(
491
+ "--mode", "-m",
492
+ choices=["default", "record"],
493
+ default="default",
494
+ help="Output mode: default (full context) or record (for record-session)",
495
+ )
496
+ ```
497
+
498
+ **When to add a new mode** (not a new script):
499
+ - Output is a subset/reordering of the same data
500
+ - The underlying data sources are shared
501
+ - The difference is in presentation, not in data fetching
502
+
503
+ ---
504
+
323
505
  ## Error Handling
324
506
 
325
507
  ### Exit Codes
@@ -387,6 +387,19 @@ Here's my understanding of the complete requirements:
387
387
  Does this look correct? If yes, I'll proceed with implementation.
388
388
  ```
389
389
 
390
+ ### Subtask Decomposition (Complex Tasks)
391
+
392
+ For complex tasks with multiple independent work items, create subtasks:
393
+
394
+ ```bash
395
+ # Create child tasks
396
+ CHILD1=$(python3 ./.trellis/scripts/task.py create "Child task 1" --slug child1 --parent "$TASK_DIR")
397
+ CHILD2=$(python3 ./.trellis/scripts/task.py create "Child task 2" --slug child2 --parent "$TASK_DIR")
398
+
399
+ # Or link existing tasks
400
+ python3 ./.trellis/scripts/task.py add-subtask "$TASK_DIR" "$CHILD_DIR"
401
+ ```
402
+
390
403
  ---
391
404
 
392
405
  ## PRD Target Structure (final)
@@ -12,7 +12,10 @@
12
12
  python3 ./.trellis/scripts/get_context.py --mode record
13
13
  ```
14
14
 
15
- [!] If MY ACTIVE TASKS shows any completed tasks, archive them FIRST:
15
+ [!] Archive tasks whose work is **actually done** — judge by work status, not the `status` field in task.json:
16
+ - Code committed? → Archive it (don't wait for PR)
17
+ - All acceptance criteria met? → Archive it
18
+ - Don't skip archiving just because `status` still says `planning` or `in_progress`
16
19
 
17
20
  ```bash
18
21
  python3 ./.trellis/scripts/task.py archive <task-name>
@@ -93,6 +93,10 @@ See `/trellis:brainstorm` for the full process. Summary:
93
93
  5. **Confirm final requirements** - Get explicit approval
94
94
  6. **Proceed to Task Workflow** - With clear requirements in PRD
95
95
 
96
+ > **Subtask Decomposition**: If brainstorm reveals multiple independent work items,
97
+ > consider creating subtasks using `--parent` flag or `add-subtask` command.
98
+ > See `/trellis:brainstorm` Step 8 for details.
99
+
96
100
  ---
97
101
 
98
102
  ## Task Workflow (Development Tasks)
@@ -392,6 +392,19 @@ Here's my understanding of the complete requirements:
392
392
  Does this look correct? If yes, I'll proceed with implementation.
393
393
  ```
394
394
 
395
+ ### Subtask Decomposition (Complex Tasks)
396
+
397
+ For complex tasks with multiple independent work items, create subtasks:
398
+
399
+ ```bash
400
+ # Create child tasks
401
+ CHILD1=$(python3 ./.trellis/scripts/task.py create "Child task 1" --slug child1 --parent "$TASK_DIR")
402
+ CHILD2=$(python3 ./.trellis/scripts/task.py create "Child task 2" --slug child2 --parent "$TASK_DIR")
403
+
404
+ # Or link existing tasks
405
+ python3 ./.trellis/scripts/task.py add-subtask "$TASK_DIR" "$CHILD_DIR"
406
+ ```
407
+
395
408
  ---
396
409
 
397
410
  ## PRD Target Structure (final)