@mindfoldhq/trellis 0.5.0-beta.7 → 0.5.0-beta.9

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 (51) hide show
  1. package/dist/commands/init.d.ts +10 -0
  2. package/dist/commands/init.d.ts.map +1 -1
  3. package/dist/commands/init.js +380 -116
  4. package/dist/commands/init.js.map +1 -1
  5. package/dist/commands/update.d.ts.map +1 -1
  6. package/dist/commands/update.js +6 -21
  7. package/dist/commands/update.js.map +1 -1
  8. package/dist/configurators/index.d.ts.map +1 -1
  9. package/dist/configurators/index.js +10 -8
  10. package/dist/configurators/index.js.map +1 -1
  11. package/dist/configurators/opencode.d.ts +10 -3
  12. package/dist/configurators/opencode.d.ts.map +1 -1
  13. package/dist/configurators/opencode.js +58 -32
  14. package/dist/configurators/opencode.js.map +1 -1
  15. package/dist/configurators/qoder.d.ts +7 -6
  16. package/dist/configurators/qoder.d.ts.map +1 -1
  17. package/dist/configurators/qoder.js +17 -9
  18. package/dist/configurators/qoder.js.map +1 -1
  19. package/dist/configurators/shared.d.ts +2 -0
  20. package/dist/configurators/shared.d.ts.map +1 -1
  21. package/dist/configurators/shared.js +18 -0
  22. package/dist/configurators/shared.js.map +1 -1
  23. package/dist/migrations/manifests/0.5.0-beta.8.json +9 -0
  24. package/dist/migrations/manifests/0.5.0-beta.9.json +48 -0
  25. package/dist/templates/claude/settings.json +3 -0
  26. package/dist/templates/common/skills/brainstorm.md +47 -4
  27. package/dist/templates/opencode/plugins/inject-workflow-state.js +21 -7
  28. package/dist/templates/shared-hooks/inject-workflow-state.py +21 -8
  29. package/dist/templates/shared-hooks/session-start.py +14 -4
  30. package/dist/templates/trellis/config.yaml +6 -0
  31. package/dist/templates/trellis/index.d.ts +0 -1
  32. package/dist/templates/trellis/index.d.ts.map +1 -1
  33. package/dist/templates/trellis/index.js +0 -2
  34. package/dist/templates/trellis/index.js.map +1 -1
  35. package/dist/templates/trellis/scripts/common/types.py +0 -2
  36. package/dist/templates/trellis/workflow.md +8 -3
  37. package/dist/utils/project-detector.d.ts +2 -0
  38. package/dist/utils/project-detector.d.ts.map +1 -1
  39. package/dist/utils/project-detector.js +120 -11
  40. package/dist/utils/project-detector.js.map +1 -1
  41. package/dist/utils/task-json.d.ts +46 -0
  42. package/dist/utils/task-json.d.ts.map +1 -0
  43. package/dist/utils/task-json.js +49 -0
  44. package/dist/utils/task-json.js.map +1 -0
  45. package/package.json +1 -1
  46. package/dist/templates/markdown/spec/backend/directory-structure.md +0 -292
  47. package/dist/templates/markdown/spec/backend/index.md +0 -40
  48. package/dist/templates/markdown/spec/backend/script-conventions.md +0 -742
  49. package/dist/templates/markdown/spec/guides/code-reuse-thinking-guide.md +0 -118
  50. package/dist/templates/markdown/spec/guides/cross-platform-thinking-guide.md +0 -394
  51. package/dist/templates/trellis/scripts/create_bootstrap.py +0 -298
@@ -1,742 +0,0 @@
1
- # Script Conventions
2
-
3
- > Standards for Python scripts in the `.trellis/scripts/` directory.
4
-
5
- ---
6
-
7
- ## Overview
8
-
9
- All workflow scripts are written in **Python 3.10+** for cross-platform compatibility. Scripts use only the standard library (no external dependencies).
10
-
11
- ---
12
-
13
- ## Directory Structure
14
-
15
- ```
16
- .trellis/scripts/
17
- ├── __init__.py # Package init
18
- ├── common/ # Shared modules
19
- │ ├── __init__.py
20
- │ ├── paths.py # Path constants and functions
21
- │ ├── developer.py # Developer identity management
22
- │ ├── task_queue.py # Task queue CRUD
23
- │ ├── task_utils.py # Task helper functions
24
- │ ├── phase.py # Multi-agent phase tracking
25
- │ ├── registry.py # Agent registry management
26
- │ ├── config.py # Config reader (config.yaml, hooks)
27
- │ ├── worktree.py # Git worktree utilities + YAML parser
28
- │ └── git_context.py # Git/session context
29
- ├── hooks/ # Lifecycle hook scripts (project-specific)
30
- │ └── linear_sync.py # Example: sync tasks to Linear
31
- ├── multi_agent/ # Multi-agent pipeline scripts
32
- │ ├── __init__.py
33
- │ ├── start.py # Start worktree agent
34
- │ ├── status.py # Monitor agent status
35
- │ ├── plan.py # Start plan agent
36
- │ ├── cleanup.py # Cleanup worktree
37
- │ └── create_pr.py # Create PR from task
38
- ├── task.py # Main task management CLI
39
- ├── get_context.py # Session context retrieval
40
- ├── init_developer.py # Developer initialization
41
- ├── get_developer.py # Get current developer
42
- ├── add_session.py # Session recording
43
- └── create_bootstrap.py # Bootstrap task creation
44
- ```
45
-
46
- ---
47
-
48
- ## Script Types
49
-
50
- ### Library Modules (`common/*.py`)
51
-
52
- Shared utilities imported by other scripts. **Never run directly.**
53
-
54
- ```python
55
- # common/paths.py - Example library module
56
-
57
- from __future__ import annotations
58
-
59
- from pathlib import Path
60
-
61
- # Constants
62
- DIR_WORKFLOW = ".trellis"
63
- DIR_SCRIPTS = "scripts"
64
- DIR_TASKS = "tasks"
65
-
66
- def get_repo_root() -> Path | None:
67
- """Find repository root by looking for .trellis directory."""
68
- current = Path.cwd().resolve()
69
- while current != current.parent:
70
- if (current / DIR_WORKFLOW).is_dir():
71
- return current
72
- current = current.parent
73
- return None
74
- ```
75
-
76
- ### Entry Scripts (`*.py`)
77
-
78
- CLI tools that users run directly. Include docstring with usage.
79
-
80
- ```python
81
- #!/usr/bin/env python3
82
- """
83
- Task Management Script.
84
-
85
- Usage:
86
- python3 task.py create "<title>" [--slug <name>]
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>
91
- python3 task.py start <dir>
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]
97
- python3 task.py archive <task-name>
98
- python3 task.py list [--mine] [--status <status>]
99
- python3 task.py list-archive [YYYY-MM]
100
- """
101
-
102
- from __future__ import annotations
103
-
104
- import argparse
105
- import sys
106
- from pathlib import Path
107
-
108
- from common.paths import get_repo_root, DIR_WORKFLOW
109
-
110
-
111
- def main() -> int:
112
- """Main entry point."""
113
- parser = argparse.ArgumentParser(description="Task management")
114
- # ... argument setup
115
- args = parser.parse_args()
116
- # ... command dispatch
117
- return 0
118
-
119
-
120
- if __name__ == "__main__":
121
- sys.exit(main())
122
- ```
123
-
124
- ---
125
-
126
- ## Coding Standards
127
-
128
- ### Type Hints
129
-
130
- Use modern type hints (Python 3.10+ syntax):
131
-
132
- ```python
133
- # Good
134
- def get_tasks(status: str | None = None) -> list[dict]:
135
- ...
136
-
137
- def read_json(path: Path) -> dict | None:
138
- ...
139
-
140
- # Bad - old style
141
- from typing import Optional, List, Dict
142
- def get_tasks(status: Optional[str] = None) -> List[Dict]:
143
- ...
144
- ```
145
-
146
- ### Path Handling
147
-
148
- Always use `pathlib.Path`:
149
-
150
- ```python
151
- # Good
152
- from pathlib import Path
153
-
154
- def read_file(path: Path) -> str:
155
- return path.read_text(encoding="utf-8")
156
-
157
- config_path = repo_root / DIR_WORKFLOW / "config.json"
158
-
159
- # Bad - string concatenation
160
- config_path = repo_root + "/" + DIR_WORKFLOW + "/config.json"
161
- ```
162
-
163
- ### JSON Operations
164
-
165
- Use helper functions for consistent error handling:
166
-
167
- ```python
168
- import json
169
- from pathlib import Path
170
-
171
-
172
- def read_json(path: Path) -> dict | None:
173
- """Read JSON file, return None on error."""
174
- try:
175
- return json.loads(path.read_text(encoding="utf-8"))
176
- except (FileNotFoundError, json.JSONDecodeError):
177
- return None
178
-
179
-
180
- def write_json(path: Path, data: dict) -> bool:
181
- """Write JSON file, return success status."""
182
- try:
183
- path.write_text(
184
- json.dumps(data, indent=2, ensure_ascii=False),
185
- encoding="utf-8"
186
- )
187
- return True
188
- except Exception:
189
- return False
190
- ```
191
-
192
- ### Subprocess Execution
193
-
194
- ```python
195
- import subprocess
196
- from pathlib import Path
197
-
198
-
199
- def run_command(
200
- cmd: list[str],
201
- cwd: Path | None = None
202
- ) -> tuple[int, str, str]:
203
- """Run command and return (returncode, stdout, stderr)."""
204
- result = subprocess.run(
205
- cmd,
206
- cwd=cwd,
207
- capture_output=True,
208
- text=True
209
- )
210
- return result.returncode, result.stdout, result.stderr
211
- ```
212
-
213
- ---
214
-
215
- ## Cross-Platform Compatibility
216
-
217
- ### CRITICAL: Windows stdio Encoding (stdout + stdin)
218
-
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`
222
-
223
- **The Problem Chain (stdout)**:
224
-
225
- ```
226
- Windows code page = GBK (936)
227
-
228
- Python stdout defaults to GBK encoding
229
-
230
- Subprocess output contains special chars → replaced with \ufffd (replacement char)
231
-
232
- json.dumps(ensure_ascii=False) → print()
233
-
234
- GBK cannot encode \ufffd → UnicodeEncodeError: 'gbk' codec can't encode character
235
- ```
236
-
237
- **The Problem Chain (stdin)**:
238
-
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
- ```
248
-
249
- **Root Cause**: Even if you set `PYTHONIOENCODING` in subprocess calls, the **parent process's stdio** still uses the system code page.
250
-
251
- ---
252
-
253
- #### GOOD: Centralize encoding fix in `common/__init__.py`
254
-
255
- All stdio encoding is handled in one place. Scripts that `from common import ...` automatically get the fix:
256
-
257
- ```python
258
- # common/__init__.py
259
- import io
260
- import sys
261
-
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
270
-
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!
275
- ```
276
-
277
- ---
278
-
279
- #### DON'T: Inline encoding code in individual scripts
280
-
281
- ```python
282
- # BAD - Duplicated in every script, easy to forget stdin
283
- import sys
284
- if sys.platform == "win32":
285
- sys.stdout.reconfigure(encoding="utf-8", errors="replace")
286
- # Forgot stdin! Piped Chinese text will break.
287
- ```
288
-
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
293
-
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.
295
-
296
- ---
297
-
298
- #### Summary
299
-
300
- | Method | Works? | Reason |
301
- |--------|--------|--------|
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 |
304
- | `io.TextIOWrapper(sys.stdout.buffer, ...)` | ❌ No | Creates wrapper, doesn't fix underlying encoding |
305
- | `PYTHONIOENCODING=utf-8` env var | ⚠️ Partial | Only works if set **before** Python starts |
306
-
307
- ### CRITICAL: Always Use `python3` Explicitly
308
-
309
- Windows does not support shebang (`#!/usr/bin/env python3`). Always document invocation with explicit `python3`:
310
-
311
- ```python
312
- # In docstrings
313
- """
314
- Usage:
315
- python3 task.py create "My Task"
316
- python3 task.py list --mine
317
- """
318
-
319
- # In error messages
320
- print("Usage: python3 task.py <command>")
321
- print("Run: python3 ./.trellis/scripts/init_developer.py <name>")
322
-
323
- # In help text
324
- print("Next steps:")
325
- print(" python3 task.py start <dir>")
326
- ```
327
-
328
- ### Path Separators
329
-
330
- Use `pathlib.Path` - it handles separators automatically:
331
-
332
- ```python
333
- # Good - works on all platforms
334
- path = Path(".trellis") / "scripts" / "task.py"
335
-
336
- # Bad - Unix-only
337
- path = ".trellis/scripts/task.py"
338
- ```
339
-
340
- ---
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
-
505
- ## Parsing Structured Command Output
506
-
507
- ### CRITICAL: Preserve Semantic Whitespace
508
-
509
- Many CLI tools encode status information in leading/trailing whitespace characters. **Never blindly `.strip()` before parsing.**
510
-
511
- **Example — `git submodule status` output format**:
512
-
513
- ```
514
- abc1234 path/to/submodule (v1.0) ← space prefix = initialized
515
- -def5678 path/to/other (v2.0) ← minus prefix = not initialized
516
- +ghi9012 path/to/modified (v3.0) ← plus prefix = modified (out of sync)
517
- ```
518
-
519
- ```python
520
- # BAD — .strip() removes the leading space that means "initialized"
521
- status_line = status_out.strip()
522
- prefix = status_line[0] # Reads commit hash char, not status prefix!
523
-
524
- # GOOD — parse the raw line, then strip individual fields
525
- raw_line = status_out.rstrip("\n") # Only remove trailing newline
526
- if not raw_line:
527
- continue
528
- prefix = raw_line[0] # ' ', '-', or '+'
529
- rest = raw_line[1:].strip() # Now safe to strip the rest
530
- commit_hash = rest.split()[0]
531
- ```
532
-
533
- **General rule**: When a command's output uses positional formatting (columns, prefixes, fixed-width fields), parse the structure first, then clean up individual values.
534
-
535
- **Other commands with semantic whitespace**:
536
- - `git status --porcelain` — two-char status prefix (`XY`)
537
- - `git diff --name-status` — tab-separated with status prefix
538
- - `docker ps --format` — column-aligned output
539
-
540
- ---
541
-
542
- ## Monorepo Config API (`common/config.py`)
543
-
544
- ### Config Functions
545
-
546
- | Function | Return | Purpose |
547
- |----------|--------|---------|
548
- | `is_monorepo(repo_root)` | `bool` | Whether `packages:` exists in config.yaml |
549
- | `get_packages(repo_root)` | `dict[str, dict] \| None` | All packages from config.yaml (`{name: {path, type?}}`) |
550
- | `get_default_package(repo_root)` | `str \| None` | The `default_package` from config.yaml |
551
- | `get_submodule_packages(repo_root)` | `dict[str, str]` | Packages with `type: submodule` (`{name: path}`) |
552
- | `get_spec_base(package, repo_root)` | `str` | `"spec"` (single-repo) or `"spec/<package>"` (monorepo) |
553
- | `validate_package(package, repo_root)` | `bool` | Whether package exists in config (always `True` for single-repo) |
554
- | `resolve_package(task_pkg, repo_root)` | `str \| None` | Resolve package: task → default → None |
555
- | `get_spec_scope(repo_root)` | `str \| list \| None` | The `session.spec_scope` config value |
556
- | `get_hooks(event, repo_root)` | `list[str]` | Hook commands for lifecycle event |
557
-
558
- ### Config.yaml Schema
559
-
560
- ```yaml
561
- # Auto-detected monorepo packages (written by trellis init)
562
- packages:
563
- cli:
564
- path: packages/cli
565
- docs-site:
566
- path: docs-site
567
- type: submodule # optional, marks git submodule
568
- default_package: cli # first non-submodule package
569
-
570
- # Session behavior
571
- session:
572
- spec_scope: active_task # or ["cli", "docs-site"] or omit for full scan
573
-
574
- # Update behavior
575
- update:
576
- skip:
577
- - .claude/commands/trellis/my-custom.md
578
-
579
- # Lifecycle hooks
580
- hooks:
581
- after_create:
582
- - "python3 .trellis/scripts/hooks/my_hook.py create"
583
- ```
584
-
585
- ### Worktree Submodule Initialization
586
-
587
- When `start.py` creates a worktree for a task, it calls `_init_submodules_for_task()`:
588
-
589
- 1. Read `packages` from config.yaml via `get_packages()`
590
- 2. Resolve target package from task data or `default_package`
591
- 3. Check if the package is a submodule via `get_submodule_packages()`
592
- 4. Run `git submodule status <path>` in the worktree
593
- 5. Parse the status prefix (see "Parsing Structured Command Output" above)
594
- 6. If uninitialized (`-` prefix): run `git submodule update --init <path>`
595
-
596
- ---
597
-
598
- ## Error Handling
599
-
600
- ### Exit Codes
601
-
602
- | Code | Meaning |
603
- |------|---------|
604
- | 0 | Success |
605
- | 1 | General error |
606
- | 2 | Usage error (wrong arguments) |
607
-
608
- ### Error Messages
609
-
610
- Print errors to stderr with context:
611
-
612
- ```python
613
- import sys
614
-
615
- def error(msg: str) -> None:
616
- """Print error message to stderr."""
617
- print(f"Error: {msg}", file=sys.stderr)
618
-
619
- # Usage
620
- if not repo_root:
621
- error("Not in a Trellis project (no .trellis directory found)")
622
- sys.exit(1)
623
- ```
624
-
625
- ---
626
-
627
- ## Argument Parsing
628
-
629
- Use `argparse` for consistent CLI interface:
630
-
631
- ```python
632
- import argparse
633
-
634
-
635
- def main() -> int:
636
- parser = argparse.ArgumentParser(
637
- description="Task management",
638
- formatter_class=argparse.RawDescriptionHelpFormatter,
639
- epilog="""
640
- Examples:
641
- python3 task.py create "Add login" --slug add-login
642
- python3 task.py list --mine --status in_progress
643
- """
644
- )
645
-
646
- subparsers = parser.add_subparsers(dest="command", required=True)
647
-
648
- # create command
649
- create_parser = subparsers.add_parser("create", help="Create new task")
650
- create_parser.add_argument("title", help="Task title")
651
- create_parser.add_argument("--slug", help="URL-friendly name")
652
-
653
- # list command
654
- list_parser = subparsers.add_parser("list", help="List tasks")
655
- list_parser.add_argument("--mine", "-m", action="store_true")
656
- list_parser.add_argument("--status", "-s", choices=["planning", "in_progress", "review", "completed"])
657
-
658
- args = parser.parse_args()
659
-
660
- if args.command == "create":
661
- return cmd_create(args)
662
- elif args.command == "list":
663
- return cmd_list(args)
664
-
665
- return 0
666
- ```
667
-
668
- ---
669
-
670
- ## Import Conventions
671
-
672
- ### Relative Imports Within Package
673
-
674
- ```python
675
- # In task.py (root level)
676
- from common.paths import get_repo_root, DIR_WORKFLOW
677
- from common.developer import get_developer
678
-
679
- # In common/developer.py
680
- from .paths import get_repo_root, DIR_WORKFLOW
681
- ```
682
-
683
- ### Standard Library Imports
684
-
685
- Group and order imports:
686
-
687
- ```python
688
- # 1. Future imports
689
- from __future__ import annotations
690
-
691
- # 2. Standard library
692
- import argparse
693
- import json
694
- import os
695
- import subprocess
696
- import sys
697
- from datetime import datetime
698
- from pathlib import Path
699
-
700
- # 3. Local imports
701
- from common.paths import get_repo_root
702
- from common.developer import get_developer
703
- ```
704
-
705
- ---
706
-
707
- ## DO / DON'T
708
-
709
- ### DO
710
-
711
- - Use `pathlib.Path` for all path operations
712
- - Use type hints (Python 3.10+ syntax)
713
- - Return exit codes from `main()`
714
- - Print errors to stderr
715
- - Always use `python3` in documentation and messages
716
- - Use `encoding="utf-8"` for all file operations
717
-
718
- ### DON'T
719
-
720
- - Don't use string path concatenation
721
- - Don't use `os.path` when `pathlib` works
722
- - Don't rely on shebang for invocation documentation
723
- - Don't use `print()` for errors (use stderr)
724
- - Don't hardcode paths - use constants from `common/paths.py`
725
- - Don't use external dependencies (stdlib only)
726
-
727
- ---
728
-
729
- ## Example: Complete Script
730
-
731
- See `.trellis/scripts/task.py` for a comprehensive example with:
732
- - Multiple subcommands
733
- - Argument parsing
734
- - JSON file operations
735
- - Error handling
736
- - Cross-platform path handling
737
-
738
- ---
739
-
740
- ## Migration Note
741
-
742
- > **Historical Context**: Scripts were migrated from Bash to Python in v0.3.0 for cross-platform compatibility. The old shell scripts are archived in `.trellis/scripts-shell-archive/` (if preserved).