@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.
- package/README.md +6 -4
- 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 +243 -49
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +3 -0
- package/dist/commands/update.js.map +1 -1
- package/dist/migrations/manifests/0.3.4.json +1 -1
- package/dist/migrations/manifests/0.3.5.json +9 -0
- package/dist/migrations/manifests/0.3.6.json +9 -0
- package/dist/templates/claude/commands/trellis/brainstorm.md +13 -0
- package/dist/templates/claude/commands/trellis/record-session.md +4 -1
- package/dist/templates/claude/commands/trellis/start.md +4 -0
- package/dist/templates/claude/hooks/inject-subagent-context.py +1 -1
- package/dist/templates/claude/settings.json +10 -0
- package/dist/templates/codex/skills/brainstorm/SKILL.md +13 -0
- package/dist/templates/codex/skills/record-session/SKILL.md +4 -1
- package/dist/templates/codex/skills/start/SKILL.md +4 -0
- package/dist/templates/cursor/commands/trellis-brainstorm.md +13 -0
- package/dist/templates/cursor/commands/trellis-record-session.md +4 -1
- package/dist/templates/cursor/commands/trellis-start.md +4 -0
- package/dist/templates/gemini/commands/trellis/brainstorm.toml +15 -0
- package/dist/templates/gemini/commands/trellis/record-session.toml +4 -1
- package/dist/templates/gemini/commands/trellis/start.toml +4 -0
- package/dist/templates/iflow/commands/trellis/brainstorm.md +13 -0
- package/dist/templates/iflow/commands/trellis/record-session.md +4 -1
- package/dist/templates/iflow/commands/trellis/start.md +4 -0
- package/dist/templates/iflow/hooks/inject-subagent-context.py +1 -1
- package/dist/templates/kilo/workflows/brainstorm.md +13 -0
- package/dist/templates/kilo/workflows/record-session.md +4 -1
- package/dist/templates/kilo/workflows/start.md +4 -0
- package/dist/templates/kiro/skills/brainstorm/SKILL.md +13 -0
- package/dist/templates/kiro/skills/record-session/SKILL.md +4 -1
- package/dist/templates/kiro/skills/start/SKILL.md +4 -0
- package/dist/templates/markdown/spec/backend/directory-structure.md +292 -0
- package/dist/templates/markdown/spec/backend/script-conventions.md +220 -38
- package/dist/templates/opencode/commands/trellis/brainstorm.md +13 -0
- package/dist/templates/opencode/commands/trellis/record-session.md +4 -1
- package/dist/templates/opencode/commands/trellis/start.md +4 -0
- package/dist/templates/qoder/skills/brainstorm/SKILL.md +13 -0
- package/dist/templates/qoder/skills/record-session/SKILL.md +4 -1
- package/dist/templates/qoder/skills/start/SKILL.md +4 -0
- package/dist/templates/trellis/config.yaml +18 -0
- package/dist/templates/trellis/scripts/common/config.py +20 -0
- package/dist/templates/trellis/scripts/common/git_context.py +160 -12
- package/dist/templates/trellis/scripts/common/task_queue.py +4 -0
- package/dist/templates/trellis/scripts/common/worktree.py +78 -11
- package/dist/templates/trellis/scripts/create_bootstrap.py +3 -0
- package/dist/templates/trellis/scripts/task.py +312 -17
- package/dist/utils/template-fetcher.d.ts +57 -4
- package/dist/utils/template-fetcher.d.ts.map +1 -1
- package/dist/utils/template-fetcher.js +179 -10
- package/dist/utils/template-fetcher.js.map +1 -1
- 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
|
-
│ ├──
|
|
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
|
|
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
|
|
217
|
+
### CRITICAL: Windows stdio Encoding (stdout + stdin)
|
|
206
218
|
|
|
207
|
-
On Windows, Python's stdout
|
|
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
|
-
**
|
|
224
|
-
|
|
225
|
-
---
|
|
237
|
+
**The Problem Chain (stdin)**:
|
|
226
238
|
|
|
227
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
253
|
+
#### GOOD: Centralize encoding fix in `common/__init__.py`
|
|
239
254
|
|
|
240
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
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
|
-
####
|
|
279
|
+
#### DON'T: Inline encoding code in individual scripts
|
|
256
280
|
|
|
257
281
|
```python
|
|
258
|
-
# BAD -
|
|
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
|
|
285
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
286
|
+
# Forgot stdin! Piped Chinese text will break.
|
|
264
287
|
```
|
|
265
288
|
|
|
266
|
-
**Why this
|
|
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
|
-
|
|
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
|
-
| `
|
|
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
|
-
[!]
|
|
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)
|