@mindfoldhq/trellis 0.3.8 → 0.3.10-beta.0

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 (174) hide show
  1. package/dist/cli/index.js +2 -0
  2. package/dist/cli/index.js.map +1 -1
  3. package/dist/commands/init.d.ts +1 -0
  4. package/dist/commands/init.d.ts.map +1 -1
  5. package/dist/commands/init.js +203 -31
  6. package/dist/commands/init.js.map +1 -1
  7. package/dist/commands/update.d.ts.map +1 -1
  8. package/dist/commands/update.js +154 -6
  9. package/dist/commands/update.js.map +1 -1
  10. package/dist/configurators/workflow.d.ts +6 -2
  11. package/dist/configurators/workflow.d.ts.map +1 -1
  12. package/dist/configurators/workflow.js +88 -58
  13. package/dist/configurators/workflow.js.map +1 -1
  14. package/dist/migrations/index.d.ts +1 -0
  15. package/dist/migrations/index.d.ts.map +1 -1
  16. package/dist/migrations/index.js +2 -0
  17. package/dist/migrations/index.js.map +1 -1
  18. package/dist/migrations/manifests/0.3.9.json +9 -0
  19. package/dist/migrations/manifests/0.4.0-beta.1.json +228 -0
  20. package/dist/templates/claude/agents/dispatch.md +1 -2
  21. package/dist/templates/claude/agents/implement.md +2 -3
  22. package/dist/templates/claude/commands/trellis/before-dev.md +29 -0
  23. package/dist/templates/claude/commands/trellis/check.md +25 -0
  24. package/dist/templates/claude/commands/trellis/create-command.md +2 -2
  25. package/dist/templates/claude/commands/trellis/onboard.md +13 -13
  26. package/dist/templates/claude/commands/trellis/parallel.md +1 -2
  27. package/dist/templates/claude/commands/trellis/record-session.md +1 -1
  28. package/dist/templates/claude/commands/trellis/start.md +8 -4
  29. package/dist/templates/claude/hooks/inject-subagent-context.py +21 -13
  30. package/dist/templates/claude/hooks/session-start.py +170 -2
  31. package/dist/templates/codex/skills/before-dev/SKILL.md +34 -0
  32. package/dist/templates/codex/skills/check/SKILL.md +30 -0
  33. package/dist/templates/codex/skills/create-command/SKILL.md +2 -2
  34. package/dist/templates/codex/skills/onboard/SKILL.md +11 -11
  35. package/dist/templates/codex/skills/record-session/SKILL.md +1 -1
  36. package/dist/templates/codex/skills/start/SKILL.md +8 -3
  37. package/dist/templates/cursor/commands/trellis-before-dev.md +29 -0
  38. package/dist/templates/cursor/commands/trellis-check.md +25 -0
  39. package/dist/templates/cursor/commands/trellis-create-command.md +2 -2
  40. package/dist/templates/cursor/commands/trellis-onboard.md +13 -13
  41. package/dist/templates/cursor/commands/trellis-record-session.md +1 -1
  42. package/dist/templates/cursor/commands/trellis-start.md +7 -16
  43. package/dist/templates/gemini/commands/trellis/before-dev.toml +33 -0
  44. package/dist/templates/gemini/commands/trellis/check.toml +29 -0
  45. package/dist/templates/gemini/commands/trellis/create-command.toml +2 -2
  46. package/dist/templates/gemini/commands/trellis/onboard.toml +2 -2
  47. package/dist/templates/gemini/commands/trellis/record-session.toml +1 -1
  48. package/dist/templates/gemini/commands/trellis/start.toml +9 -4
  49. package/dist/templates/iflow/agents/dispatch.md +1 -2
  50. package/dist/templates/iflow/agents/implement.md +2 -3
  51. package/dist/templates/iflow/commands/trellis/before-dev.md +29 -0
  52. package/dist/templates/iflow/commands/trellis/check.md +25 -0
  53. package/dist/templates/iflow/commands/trellis/create-command.md +2 -2
  54. package/dist/templates/iflow/commands/trellis/onboard.md +13 -13
  55. package/dist/templates/iflow/commands/trellis/parallel.md +1 -2
  56. package/dist/templates/iflow/commands/trellis/record-session.md +1 -1
  57. package/dist/templates/iflow/commands/trellis/start.md +8 -4
  58. package/dist/templates/iflow/hooks/inject-subagent-context.py +21 -13
  59. package/dist/templates/iflow/hooks/session-start.py +156 -1
  60. package/dist/templates/iflow/settings.json +2 -2
  61. package/dist/templates/kilo/workflows/before-dev.md +29 -0
  62. package/dist/templates/kilo/workflows/check.md +25 -0
  63. package/dist/templates/kilo/workflows/create-command.md +2 -2
  64. package/dist/templates/kilo/workflows/onboard.md +13 -13
  65. package/dist/templates/kilo/workflows/parallel.md +1 -2
  66. package/dist/templates/kilo/workflows/record-session.md +1 -1
  67. package/dist/templates/kilo/workflows/start.md +8 -3
  68. package/dist/templates/kiro/skills/before-dev/SKILL.md +34 -0
  69. package/dist/templates/kiro/skills/check/SKILL.md +30 -0
  70. package/dist/templates/kiro/skills/create-command/SKILL.md +2 -2
  71. package/dist/templates/kiro/skills/onboard/SKILL.md +11 -11
  72. package/dist/templates/kiro/skills/record-session/SKILL.md +1 -1
  73. package/dist/templates/kiro/skills/start/SKILL.md +8 -3
  74. package/dist/templates/markdown/spec/backend/script-conventions.md +93 -0
  75. package/dist/templates/opencode/agents/dispatch.md +1 -2
  76. package/dist/templates/opencode/agents/implement.md +2 -2
  77. package/dist/templates/opencode/agents/research.md +1 -2
  78. package/dist/templates/opencode/commands/trellis/before-dev.md +29 -0
  79. package/dist/templates/opencode/commands/trellis/check.md +25 -0
  80. package/dist/templates/opencode/commands/trellis/create-command.md +2 -2
  81. package/dist/templates/opencode/commands/trellis/onboard.md +13 -13
  82. package/dist/templates/opencode/commands/trellis/parallel.md +1 -2
  83. package/dist/templates/opencode/commands/trellis/record-session.md +1 -1
  84. package/dist/templates/opencode/commands/trellis/start.md +8 -3
  85. package/dist/templates/opencode/plugin/inject-subagent-context.js +45 -18
  86. package/dist/templates/opencode/plugin/session-start.js +149 -1
  87. package/dist/templates/qoder/skills/before-dev/SKILL.md +34 -0
  88. package/dist/templates/qoder/skills/check/SKILL.md +30 -0
  89. package/dist/templates/qoder/skills/create-command/SKILL.md +2 -2
  90. package/dist/templates/qoder/skills/onboard/SKILL.md +13 -13
  91. package/dist/templates/qoder/skills/record-session/SKILL.md +1 -1
  92. package/dist/templates/qoder/skills/start/SKILL.md +8 -3
  93. package/dist/templates/trellis/config.yaml +20 -0
  94. package/dist/templates/trellis/index.d.ts +11 -0
  95. package/dist/templates/trellis/index.d.ts.map +1 -1
  96. package/dist/templates/trellis/index.js +22 -0
  97. package/dist/templates/trellis/index.js.map +1 -1
  98. package/dist/templates/trellis/scripts/add_session.py +52 -7
  99. package/dist/templates/trellis/scripts/common/cli_adapter.py +33 -45
  100. package/dist/templates/trellis/scripts/common/config.py +152 -0
  101. package/dist/templates/trellis/scripts/common/git.py +31 -0
  102. package/dist/templates/trellis/scripts/common/git_context.py +23 -586
  103. package/dist/templates/trellis/scripts/common/io.py +37 -0
  104. package/dist/templates/trellis/scripts/common/log.py +45 -0
  105. package/dist/templates/trellis/scripts/common/packages_context.py +233 -0
  106. package/dist/templates/trellis/scripts/common/paths.py +46 -0
  107. package/dist/templates/trellis/scripts/common/phase.py +50 -49
  108. package/dist/templates/trellis/scripts/common/registry.py +41 -72
  109. package/dist/templates/trellis/scripts/common/session_context.py +466 -0
  110. package/dist/templates/trellis/scripts/common/task_context.py +384 -0
  111. package/dist/templates/trellis/scripts/common/task_queue.py +27 -98
  112. package/dist/templates/trellis/scripts/common/task_store.py +534 -0
  113. package/dist/templates/trellis/scripts/common/task_utils.py +96 -6
  114. package/dist/templates/trellis/scripts/common/tasks.py +109 -0
  115. package/dist/templates/trellis/scripts/common/types.py +112 -0
  116. package/dist/templates/trellis/scripts/create_bootstrap.py +31 -26
  117. package/dist/templates/trellis/scripts/hooks/linear_sync.py +243 -0
  118. package/dist/templates/trellis/scripts/multi_agent/_bootstrap.py +17 -0
  119. package/dist/templates/trellis/scripts/multi_agent/cleanup.py +43 -48
  120. package/dist/templates/trellis/scripts/multi_agent/create_pr.py +336 -45
  121. package/dist/templates/trellis/scripts/multi_agent/plan.py +2 -26
  122. package/dist/templates/trellis/scripts/multi_agent/start.py +126 -57
  123. package/dist/templates/trellis/scripts/multi_agent/status.py +12 -753
  124. package/dist/templates/trellis/scripts/multi_agent/status_display.py +542 -0
  125. package/dist/templates/trellis/scripts/multi_agent/status_monitor.py +225 -0
  126. package/dist/templates/trellis/scripts/task.py +50 -975
  127. package/dist/templates/trellis/workflow.md +21 -34
  128. package/dist/types/migration.d.ts +3 -1
  129. package/dist/types/migration.d.ts.map +1 -1
  130. package/dist/utils/project-detector.d.ts +23 -0
  131. package/dist/utils/project-detector.d.ts.map +1 -1
  132. package/dist/utils/project-detector.js +364 -0
  133. package/dist/utils/project-detector.js.map +1 -1
  134. package/dist/utils/template-fetcher.d.ts +2 -2
  135. package/dist/utils/template-fetcher.d.ts.map +1 -1
  136. package/dist/utils/template-fetcher.js +5 -5
  137. package/dist/utils/template-fetcher.js.map +1 -1
  138. package/package.json +1 -1
  139. package/dist/templates/claude/commands/trellis/before-backend-dev.md +0 -13
  140. package/dist/templates/claude/commands/trellis/before-frontend-dev.md +0 -13
  141. package/dist/templates/claude/commands/trellis/check-backend.md +0 -13
  142. package/dist/templates/claude/commands/trellis/check-frontend.md +0 -13
  143. package/dist/templates/codex/skills/before-backend-dev/SKILL.md +0 -18
  144. package/dist/templates/codex/skills/before-frontend-dev/SKILL.md +0 -18
  145. package/dist/templates/codex/skills/check-backend/SKILL.md +0 -18
  146. package/dist/templates/codex/skills/check-frontend/SKILL.md +0 -18
  147. package/dist/templates/cursor/commands/trellis-before-backend-dev.md +0 -13
  148. package/dist/templates/cursor/commands/trellis-before-frontend-dev.md +0 -13
  149. package/dist/templates/cursor/commands/trellis-check-backend.md +0 -13
  150. package/dist/templates/cursor/commands/trellis-check-frontend.md +0 -13
  151. package/dist/templates/gemini/commands/trellis/before-backend-dev.toml +0 -17
  152. package/dist/templates/gemini/commands/trellis/before-frontend-dev.toml +0 -17
  153. package/dist/templates/gemini/commands/trellis/check-backend.toml +0 -17
  154. package/dist/templates/gemini/commands/trellis/check-frontend.toml +0 -17
  155. package/dist/templates/iflow/commands/trellis/before-backend-dev.md +0 -13
  156. package/dist/templates/iflow/commands/trellis/before-frontend-dev.md +0 -13
  157. package/dist/templates/iflow/commands/trellis/check-backend.md +0 -13
  158. package/dist/templates/iflow/commands/trellis/check-frontend.md +0 -13
  159. package/dist/templates/kilo/workflows/before-backend-dev.md +0 -13
  160. package/dist/templates/kilo/workflows/before-frontend-dev.md +0 -13
  161. package/dist/templates/kilo/workflows/check-backend.md +0 -13
  162. package/dist/templates/kilo/workflows/check-frontend.md +0 -13
  163. package/dist/templates/kiro/skills/before-backend-dev/SKILL.md +0 -18
  164. package/dist/templates/kiro/skills/before-frontend-dev/SKILL.md +0 -18
  165. package/dist/templates/kiro/skills/check-backend/SKILL.md +0 -18
  166. package/dist/templates/kiro/skills/check-frontend/SKILL.md +0 -18
  167. package/dist/templates/opencode/commands/trellis/before-backend-dev.md +0 -13
  168. package/dist/templates/opencode/commands/trellis/before-frontend-dev.md +0 -13
  169. package/dist/templates/opencode/commands/trellis/check-backend.md +0 -13
  170. package/dist/templates/opencode/commands/trellis/check-frontend.md +0 -13
  171. package/dist/templates/qoder/skills/before-backend-dev/SKILL.md +0 -18
  172. package/dist/templates/qoder/skills/before-frontend-dev/SKILL.md +0 -18
  173. package/dist/templates/qoder/skills/check-backend/SKILL.md +0 -18
  174. package/dist/templates/qoder/skills/check-frontend/SKILL.md +0 -18
@@ -4,8 +4,8 @@
4
4
  Task Management Script for Multi-Agent Pipeline.
5
5
 
6
6
  Usage:
7
- python3 task.py create "<title>" [--slug <name>] [--assignee <dev>] [--priority P0|P1|P2|P3] [--parent <dir>]
8
- python3 task.py init-context <dir> <type> # Initialize jsonl files
7
+ python3 task.py create "<title>" [--slug <name>] [--assignee <dev>] [--priority P0|P1|P2|P3] [--parent <dir>] [--package <pkg>]
8
+ python3 task.py init-context <dir> <type> [--package <pkg>] # Initialize jsonl files
9
9
  python3 task.py add-context <dir> <file> <path> [reason] # Add jsonl entry
10
10
  python3 task.py validate <dir> # Validate jsonl files
11
11
  python3 task.py list-context <dir> # List jsonl entries
@@ -24,31 +24,14 @@ Usage:
24
24
 
25
25
  from __future__ import annotations
26
26
 
27
- import sys
28
-
29
- # IMPORTANT: Force stdout to use UTF-8 on Windows
30
- # This fixes UnicodeEncodeError when outputting non-ASCII characters
31
- if sys.platform == "win32":
32
- import io as _io
33
- if hasattr(sys.stdout, "reconfigure"):
34
- sys.stdout.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr]
35
- elif hasattr(sys.stdout, "detach"):
36
- sys.stdout = _io.TextIOWrapper(sys.stdout.detach(), encoding="utf-8", errors="replace") # type: ignore[union-attr]
37
-
38
27
  import argparse
39
- import json
40
- import re
41
28
  import sys
42
- from datetime import datetime
43
29
  from pathlib import Path
44
30
 
45
- from common.cli_adapter import get_cli_adapter_auto
46
- from common.git_context import _run_git_command
31
+ from common.log import Colors, colored
47
32
  from common.paths import (
48
33
  DIR_WORKFLOW,
49
34
  DIR_TASKS,
50
- DIR_SPEC,
51
- DIR_ARCHIVE,
52
35
  FILE_TASK_JSON,
53
36
  get_repo_root,
54
37
  get_developer,
@@ -56,585 +39,26 @@ from common.paths import (
56
39
  get_current_task,
57
40
  set_current_task,
58
41
  clear_current_task,
59
- generate_task_date_prefix,
60
42
  )
61
- from common.task_utils import (
62
- find_task_by_name,
63
- archive_task_complete,
43
+ from common.task_utils import resolve_task_dir, run_task_hooks
44
+ from common.tasks import iter_active_tasks, children_progress
45
+
46
+ # Import command handlers from split modules (also re-exports for plan.py compatibility)
47
+ from common.task_store import (
48
+ cmd_create,
49
+ cmd_archive,
50
+ cmd_set_branch,
51
+ cmd_set_base_branch,
52
+ cmd_set_scope,
53
+ cmd_add_subtask,
54
+ cmd_remove_subtask,
55
+ )
56
+ from common.task_context import (
57
+ cmd_init_context,
58
+ cmd_add_context,
59
+ cmd_validate,
60
+ cmd_list_context,
64
61
  )
65
- from common.config import get_hooks
66
-
67
-
68
- # =============================================================================
69
- # Colors
70
- # =============================================================================
71
-
72
- class Colors:
73
- RED = "\033[0;31m"
74
- GREEN = "\033[0;32m"
75
- YELLOW = "\033[1;33m"
76
- BLUE = "\033[0;34m"
77
- CYAN = "\033[0;36m"
78
- NC = "\033[0m"
79
-
80
-
81
- def colored(text: str, color: str) -> str:
82
- """Apply color to text."""
83
- return f"{color}{text}{Colors.NC}"
84
-
85
-
86
- # =============================================================================
87
- # Lifecycle Hooks
88
- # =============================================================================
89
-
90
- def _run_hooks(event: str, task_json_path: Path, repo_root: Path) -> None:
91
- """Run lifecycle hooks for an event.
92
-
93
- Args:
94
- event: Event name (e.g. "after_create").
95
- task_json_path: Absolute path to the task's task.json.
96
- repo_root: Repository root for cwd and config lookup.
97
- """
98
- import os
99
- import subprocess
100
-
101
- commands = get_hooks(event, repo_root)
102
- if not commands:
103
- return
104
-
105
- env = {**os.environ, "TASK_JSON_PATH": str(task_json_path)}
106
-
107
- for cmd in commands:
108
- try:
109
- result = subprocess.run(
110
- cmd,
111
- shell=True,
112
- cwd=repo_root,
113
- env=env,
114
- capture_output=True,
115
- text=True,
116
- encoding="utf-8",
117
- errors="replace",
118
- )
119
- if result.returncode != 0:
120
- print(
121
- colored(f"[WARN] Hook failed ({event}): {cmd}", Colors.YELLOW),
122
- file=sys.stderr,
123
- )
124
- if result.stderr.strip():
125
- print(f" {result.stderr.strip()}", file=sys.stderr)
126
- except Exception as e:
127
- print(
128
- colored(f"[WARN] Hook error ({event}): {cmd} — {e}", Colors.YELLOW),
129
- file=sys.stderr,
130
- )
131
-
132
-
133
- # =============================================================================
134
- # Helper Functions
135
- # =============================================================================
136
-
137
- def _read_json_file(path: Path) -> dict | None:
138
- """Read and parse a JSON file."""
139
- try:
140
- return json.loads(path.read_text(encoding="utf-8"))
141
- except (FileNotFoundError, json.JSONDecodeError, OSError):
142
- return None
143
-
144
-
145
- def _write_json_file(path: Path, data: dict) -> bool:
146
- """Write dict to JSON file."""
147
- try:
148
- path.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")
149
- return True
150
- except (OSError, IOError):
151
- return False
152
-
153
-
154
- def _slugify(title: str) -> str:
155
- """Convert title to slug (only works with ASCII)."""
156
- result = title.lower()
157
- result = re.sub(r"[^a-z0-9]", "-", result)
158
- result = re.sub(r"-+", "-", result)
159
- result = result.strip("-")
160
- return result
161
-
162
-
163
- def _resolve_task_dir(target_dir: str, repo_root: Path) -> Path:
164
- """Resolve task directory to absolute path.
165
-
166
- Supports:
167
- - Absolute path: /path/to/task
168
- - Relative path: .trellis/tasks/01-31-my-task
169
- - Task name: my-task (uses find_task_by_name for lookup)
170
- """
171
- if not target_dir:
172
- return Path()
173
-
174
- # Absolute path
175
- if target_dir.startswith("/"):
176
- return Path(target_dir)
177
-
178
- # Relative path (contains path separator or starts with .trellis)
179
- if "/" in target_dir or target_dir.startswith(".trellis"):
180
- return repo_root / target_dir
181
-
182
- # Task name - try to find in tasks directory
183
- tasks_dir = get_tasks_dir(repo_root)
184
- found = find_task_by_name(target_dir, tasks_dir)
185
- if found:
186
- return found
187
-
188
- # Fallback to treating as relative path
189
- return repo_root / target_dir
190
-
191
-
192
- # =============================================================================
193
- # JSONL Default Content Generators
194
- # =============================================================================
195
-
196
- def get_implement_base() -> list[dict]:
197
- """Get base implement context entries."""
198
- return [
199
- {"file": f"{DIR_WORKFLOW}/workflow.md", "reason": "Project workflow and conventions"},
200
- ]
201
-
202
-
203
- def get_implement_backend() -> list[dict]:
204
- """Get backend implement context entries."""
205
- return [
206
- {"file": f"{DIR_WORKFLOW}/{DIR_SPEC}/backend/index.md", "reason": "Backend development guide"},
207
- ]
208
-
209
-
210
- def get_implement_frontend() -> list[dict]:
211
- """Get frontend implement context entries."""
212
- return [
213
- {"file": f"{DIR_WORKFLOW}/{DIR_SPEC}/frontend/index.md", "reason": "Frontend development guide"},
214
- ]
215
-
216
-
217
- def get_check_context(dev_type: str, repo_root: Path) -> list[dict]:
218
- """Get check context entries."""
219
- adapter = get_cli_adapter_auto(repo_root)
220
-
221
- entries = [
222
- {"file": adapter.get_trellis_command_path("finish-work"), "reason": "Finish work checklist"},
223
- ]
224
-
225
- if dev_type in ("backend", "fullstack"):
226
- entries.append({"file": adapter.get_trellis_command_path("check-backend"), "reason": "Backend check spec"})
227
- if dev_type in ("frontend", "fullstack"):
228
- entries.append({"file": adapter.get_trellis_command_path("check-frontend"), "reason": "Frontend check spec"})
229
-
230
- return entries
231
-
232
-
233
- def get_debug_context(dev_type: str, repo_root: Path) -> list[dict]:
234
- """Get debug context entries."""
235
- adapter = get_cli_adapter_auto(repo_root)
236
-
237
- entries: list[dict] = []
238
-
239
- if dev_type in ("backend", "fullstack"):
240
- entries.append({"file": adapter.get_trellis_command_path("check-backend"), "reason": "Backend check spec"})
241
- if dev_type in ("frontend", "fullstack"):
242
- entries.append({"file": adapter.get_trellis_command_path("check-frontend"), "reason": "Frontend check spec"})
243
-
244
- return entries
245
-
246
-
247
- def _write_jsonl(path: Path, entries: list[dict]) -> None:
248
- """Write entries to JSONL file."""
249
- lines = [json.dumps(entry, ensure_ascii=False) for entry in entries]
250
- path.write_text("\n".join(lines) + "\n", encoding="utf-8")
251
-
252
-
253
- # =============================================================================
254
- # Task Operations
255
- # =============================================================================
256
-
257
- def ensure_tasks_dir(repo_root: Path) -> Path:
258
- """Ensure tasks directory exists."""
259
- tasks_dir = get_tasks_dir(repo_root)
260
- archive_dir = tasks_dir / "archive"
261
-
262
- if not tasks_dir.exists():
263
- tasks_dir.mkdir(parents=True)
264
- print(colored(f"Created tasks directory: {tasks_dir}", Colors.GREEN), file=sys.stderr)
265
-
266
- if not archive_dir.exists():
267
- archive_dir.mkdir(parents=True)
268
-
269
- return tasks_dir
270
-
271
-
272
- # =============================================================================
273
- # Command: create
274
- # =============================================================================
275
-
276
- def cmd_create(args: argparse.Namespace) -> int:
277
- """Create a new task."""
278
- repo_root = get_repo_root()
279
-
280
- if not args.title:
281
- print(colored("Error: title is required", Colors.RED), file=sys.stderr)
282
- return 1
283
-
284
- # Default assignee to current developer
285
- assignee = args.assignee
286
- if not assignee:
287
- assignee = get_developer(repo_root)
288
- if not assignee:
289
- print(colored("Error: No developer set. Run init_developer.py first or use --assignee", Colors.RED), file=sys.stderr)
290
- return 1
291
-
292
- ensure_tasks_dir(repo_root)
293
-
294
- # Get current developer as creator
295
- creator = get_developer(repo_root) or assignee
296
-
297
- # Generate slug if not provided
298
- slug = args.slug or _slugify(args.title)
299
- if not slug:
300
- print(colored("Error: could not generate slug from title", Colors.RED), file=sys.stderr)
301
- return 1
302
-
303
- # Create task directory with MM-DD-slug format
304
- tasks_dir = get_tasks_dir(repo_root)
305
- date_prefix = generate_task_date_prefix()
306
- dir_name = f"{date_prefix}-{slug}"
307
- task_dir = tasks_dir / dir_name
308
- task_json_path = task_dir / FILE_TASK_JSON
309
-
310
- if task_dir.exists():
311
- print(colored(f"Warning: Task directory already exists: {dir_name}", Colors.YELLOW), file=sys.stderr)
312
- else:
313
- task_dir.mkdir(parents=True)
314
-
315
- today = datetime.now().strftime("%Y-%m-%d")
316
-
317
- # Record current branch as base_branch (PR target)
318
- _, branch_out, _ = _run_git_command(["branch", "--show-current"], cwd=repo_root)
319
- current_branch = branch_out.strip() or "main"
320
-
321
- task_data = {
322
- "id": slug,
323
- "name": slug,
324
- "title": args.title,
325
- "description": args.description or "",
326
- "status": "planning",
327
- "dev_type": None,
328
- "scope": None,
329
- "priority": args.priority,
330
- "creator": creator,
331
- "assignee": assignee,
332
- "createdAt": today,
333
- "completedAt": None,
334
- "branch": None,
335
- "base_branch": current_branch,
336
- "worktree_path": None,
337
- "current_phase": 0,
338
- "next_action": [
339
- {"phase": 1, "action": "implement"},
340
- {"phase": 2, "action": "check"},
341
- {"phase": 3, "action": "finish"},
342
- {"phase": 4, "action": "create-pr"},
343
- ],
344
- "commit": None,
345
- "pr_url": None,
346
- "subtasks": [],
347
- "children": [],
348
- "parent": None,
349
- "relatedFiles": [],
350
- "notes": "",
351
- "meta": {},
352
- }
353
-
354
- _write_json_file(task_json_path, task_data)
355
-
356
- # Handle --parent: establish bidirectional link
357
- if args.parent:
358
- parent_dir = _resolve_task_dir(args.parent, repo_root)
359
- parent_json_path = parent_dir / FILE_TASK_JSON
360
- if not parent_json_path.is_file():
361
- print(colored(f"Warning: Parent task.json not found: {args.parent}", Colors.YELLOW), file=sys.stderr)
362
- else:
363
- parent_data = _read_json_file(parent_json_path)
364
- if parent_data:
365
- # Add child to parent's children list
366
- parent_children = parent_data.get("children", [])
367
- if dir_name not in parent_children:
368
- parent_children.append(dir_name)
369
- parent_data["children"] = parent_children
370
- _write_json_file(parent_json_path, parent_data)
371
-
372
- # Set parent in child's task.json
373
- task_data["parent"] = parent_dir.name
374
- _write_json_file(task_json_path, task_data)
375
-
376
- print(colored(f"Linked as child of: {parent_dir.name}", Colors.GREEN), file=sys.stderr)
377
-
378
- print(colored(f"Created task: {dir_name}", Colors.GREEN), file=sys.stderr)
379
- print("", file=sys.stderr)
380
- print(colored("Next steps:", Colors.BLUE), file=sys.stderr)
381
- print(" 1. Create prd.md with requirements", file=sys.stderr)
382
- print(" 2. Run: python3 task.py init-context <dir> <dev_type>", file=sys.stderr)
383
- print(" 3. Run: python3 task.py start <dir>", file=sys.stderr)
384
- print("", file=sys.stderr)
385
-
386
- # Output relative path for script chaining
387
- print(f"{DIR_WORKFLOW}/{DIR_TASKS}/{dir_name}")
388
-
389
- _run_hooks("after_create", task_json_path, repo_root)
390
- return 0
391
-
392
-
393
- # =============================================================================
394
- # Command: init-context
395
- # =============================================================================
396
-
397
- def cmd_init_context(args: argparse.Namespace) -> int:
398
- """Initialize JSONL context files for a task."""
399
- repo_root = get_repo_root()
400
- target_dir = _resolve_task_dir(args.dir, repo_root)
401
- dev_type = args.type
402
-
403
- if not dev_type:
404
- print(colored("Error: Missing arguments", Colors.RED))
405
- print("Usage: python3 task.py init-context <task-dir> <dev_type>")
406
- print(" dev_type: backend | frontend | fullstack | test | docs")
407
- return 1
408
-
409
- if not target_dir.is_dir():
410
- print(colored(f"Error: Directory not found: {target_dir}", Colors.RED))
411
- return 1
412
-
413
- print(colored("=== Initializing Agent Context Files ===", Colors.BLUE))
414
- print(f"Target dir: {target_dir}")
415
- print(f"Dev type: {dev_type}")
416
- print()
417
-
418
- # implement.jsonl
419
- print(colored("Creating implement.jsonl...", Colors.CYAN))
420
- implement_entries = get_implement_base()
421
- if dev_type in ("backend", "test"):
422
- implement_entries.extend(get_implement_backend())
423
- elif dev_type == "frontend":
424
- implement_entries.extend(get_implement_frontend())
425
- elif dev_type == "fullstack":
426
- implement_entries.extend(get_implement_backend())
427
- implement_entries.extend(get_implement_frontend())
428
-
429
- implement_file = target_dir / "implement.jsonl"
430
- _write_jsonl(implement_file, implement_entries)
431
- print(f" {colored('✓', Colors.GREEN)} {len(implement_entries)} entries")
432
-
433
- # check.jsonl
434
- print(colored("Creating check.jsonl...", Colors.CYAN))
435
- check_entries = get_check_context(dev_type, repo_root)
436
- check_file = target_dir / "check.jsonl"
437
- _write_jsonl(check_file, check_entries)
438
- print(f" {colored('✓', Colors.GREEN)} {len(check_entries)} entries")
439
-
440
- # debug.jsonl
441
- print(colored("Creating debug.jsonl...", Colors.CYAN))
442
- debug_entries = get_debug_context(dev_type, repo_root)
443
- debug_file = target_dir / "debug.jsonl"
444
- _write_jsonl(debug_file, debug_entries)
445
- print(f" {colored('✓', Colors.GREEN)} {len(debug_entries)} entries")
446
-
447
- print()
448
- print(colored("✓ All context files created", Colors.GREEN))
449
- print()
450
- print(colored("Next steps:", Colors.BLUE))
451
- print(" 1. Add task-specific specs: python3 task.py add-context <dir> <jsonl> <path>")
452
- print(" 2. Set as current: python3 task.py start <dir>")
453
-
454
- return 0
455
-
456
-
457
- # =============================================================================
458
- # Command: add-context
459
- # =============================================================================
460
-
461
- def cmd_add_context(args: argparse.Namespace) -> int:
462
- """Add entry to JSONL context file."""
463
- repo_root = get_repo_root()
464
- target_dir = _resolve_task_dir(args.dir, repo_root)
465
-
466
- jsonl_name = args.file
467
- path = args.path
468
- reason = args.reason or "Added manually"
469
-
470
- if not target_dir.is_dir():
471
- print(colored(f"Error: Directory not found: {target_dir}", Colors.RED))
472
- return 1
473
-
474
- # Support shorthand
475
- if not jsonl_name.endswith(".jsonl"):
476
- jsonl_name = f"{jsonl_name}.jsonl"
477
-
478
- jsonl_file = target_dir / jsonl_name
479
- full_path = repo_root / path
480
-
481
- entry_type = "file"
482
- if full_path.is_dir():
483
- entry_type = "directory"
484
- if not path.endswith("/"):
485
- path = f"{path}/"
486
- elif not full_path.is_file():
487
- print(colored(f"Error: Path not found: {path}", Colors.RED))
488
- return 1
489
-
490
- # Check if already exists
491
- if jsonl_file.is_file():
492
- content = jsonl_file.read_text(encoding="utf-8")
493
- if f'"{path}"' in content:
494
- print(colored(f"Warning: Entry already exists for {path}", Colors.YELLOW))
495
- return 0
496
-
497
- # Add entry
498
- entry: dict
499
- if entry_type == "directory":
500
- entry = {"file": path, "type": "directory", "reason": reason}
501
- else:
502
- entry = {"file": path, "reason": reason}
503
-
504
- with jsonl_file.open("a", encoding="utf-8") as f:
505
- f.write(json.dumps(entry, ensure_ascii=False) + "\n")
506
-
507
- print(colored(f"Added {entry_type}: {path}", Colors.GREEN))
508
- return 0
509
-
510
-
511
- # =============================================================================
512
- # Command: validate
513
- # =============================================================================
514
-
515
- def cmd_validate(args: argparse.Namespace) -> int:
516
- """Validate JSONL context files."""
517
- repo_root = get_repo_root()
518
- target_dir = _resolve_task_dir(args.dir, repo_root)
519
-
520
- if not target_dir.is_dir():
521
- print(colored("Error: task directory required", Colors.RED))
522
- return 1
523
-
524
- print(colored("=== Validating Context Files ===", Colors.BLUE))
525
- print(f"Target dir: {target_dir}")
526
- print()
527
-
528
- total_errors = 0
529
- for jsonl_name in ["implement.jsonl", "check.jsonl", "debug.jsonl"]:
530
- jsonl_file = target_dir / jsonl_name
531
- errors = _validate_jsonl(jsonl_file, repo_root)
532
- total_errors += errors
533
-
534
- print()
535
- if total_errors == 0:
536
- print(colored("✓ All validations passed", Colors.GREEN))
537
- return 0
538
- else:
539
- print(colored(f"✗ Validation failed ({total_errors} errors)", Colors.RED))
540
- return 1
541
-
542
-
543
- def _validate_jsonl(jsonl_file: Path, repo_root: Path) -> int:
544
- """Validate a single JSONL file."""
545
- file_name = jsonl_file.name
546
- errors = 0
547
-
548
- if not jsonl_file.is_file():
549
- print(f" {colored(f'{file_name}: not found (skipped)', Colors.YELLOW)}")
550
- return 0
551
-
552
- line_num = 0
553
- for line in jsonl_file.read_text(encoding="utf-8").splitlines():
554
- line_num += 1
555
- if not line.strip():
556
- continue
557
-
558
- try:
559
- data = json.loads(line)
560
- except json.JSONDecodeError:
561
- print(f" {colored(f'{file_name}:{line_num}: Invalid JSON', Colors.RED)}")
562
- errors += 1
563
- continue
564
-
565
- file_path = data.get("file")
566
- entry_type = data.get("type", "file")
567
-
568
- if not file_path:
569
- print(f" {colored(f'{file_name}:{line_num}: Missing file field', Colors.RED)}")
570
- errors += 1
571
- continue
572
-
573
- full_path = repo_root / file_path
574
- if entry_type == "directory":
575
- if not full_path.is_dir():
576
- print(f" {colored(f'{file_name}:{line_num}: Directory not found: {file_path}', Colors.RED)}")
577
- errors += 1
578
- else:
579
- if not full_path.is_file():
580
- print(f" {colored(f'{file_name}:{line_num}: File not found: {file_path}', Colors.RED)}")
581
- errors += 1
582
-
583
- if errors == 0:
584
- print(f" {colored(f'{file_name}: ✓ ({line_num} entries)', Colors.GREEN)}")
585
- else:
586
- print(f" {colored(f'{file_name}: ✗ ({errors} errors)', Colors.RED)}")
587
-
588
- return errors
589
-
590
-
591
- # =============================================================================
592
- # Command: list-context
593
- # =============================================================================
594
-
595
- def cmd_list_context(args: argparse.Namespace) -> int:
596
- """List JSONL context entries."""
597
- repo_root = get_repo_root()
598
- target_dir = _resolve_task_dir(args.dir, repo_root)
599
-
600
- if not target_dir.is_dir():
601
- print(colored("Error: task directory required", Colors.RED))
602
- return 1
603
-
604
- print(colored("=== Context Files ===", Colors.BLUE))
605
- print()
606
-
607
- for jsonl_name in ["implement.jsonl", "check.jsonl", "debug.jsonl"]:
608
- jsonl_file = target_dir / jsonl_name
609
- if not jsonl_file.is_file():
610
- continue
611
-
612
- print(colored(f"[{jsonl_name}]", Colors.CYAN))
613
-
614
- count = 0
615
- for line in jsonl_file.read_text(encoding="utf-8").splitlines():
616
- if not line.strip():
617
- continue
618
-
619
- try:
620
- data = json.loads(line)
621
- except json.JSONDecodeError:
622
- continue
623
-
624
- count += 1
625
- file_path = data.get("file", "?")
626
- entry_type = data.get("type", "file")
627
- reason = data.get("reason", "-")
628
-
629
- if entry_type == "directory":
630
- print(f" {colored(f'{count}.', Colors.GREEN)} [DIR] {file_path}")
631
- else:
632
- print(f" {colored(f'{count}.', Colors.GREEN)} {file_path}")
633
- print(f" {colored('→', Colors.YELLOW)} {reason}")
634
-
635
- print()
636
-
637
- return 0
638
62
 
639
63
 
640
64
  # =============================================================================
@@ -651,7 +75,7 @@ def cmd_start(args: argparse.Namespace) -> int:
651
75
  return 1
652
76
 
653
77
  # Resolve task directory (supports task name, relative path, or absolute path)
654
- full_path = _resolve_task_dir(task_input, repo_root)
78
+ full_path = resolve_task_dir(task_input, repo_root)
655
79
 
656
80
  if not full_path.is_dir():
657
81
  print(colored(f"Error: Task not found: {task_input}", Colors.RED))
@@ -670,7 +94,7 @@ def cmd_start(args: argparse.Namespace) -> int:
670
94
  print(colored("The hook will now inject context from this task's jsonl files.", Colors.BLUE))
671
95
 
672
96
  task_json_path = full_path / FILE_TASK_JSON
673
- _run_hooks("after_start", task_json_path, repo_root)
97
+ run_task_hooks("after_start", task_json_path, repo_root)
674
98
  return 0
675
99
  else:
676
100
  print(colored("Error: Failed to set current task", Colors.RED))
@@ -693,221 +117,7 @@ def cmd_finish(args: argparse.Namespace) -> int:
693
117
  print(colored(f"✓ Cleared current task (was: {current})", Colors.GREEN))
694
118
 
695
119
  if task_json_path.is_file():
696
- _run_hooks("after_finish", task_json_path, repo_root)
697
- return 0
698
-
699
-
700
- # =============================================================================
701
- # Command: archive
702
- # =============================================================================
703
-
704
- def cmd_archive(args: argparse.Namespace) -> int:
705
- """Archive completed task."""
706
- repo_root = get_repo_root()
707
- task_name = args.name
708
-
709
- if not task_name:
710
- print(colored("Error: Task name is required", Colors.RED), file=sys.stderr)
711
- return 1
712
-
713
- tasks_dir = get_tasks_dir(repo_root)
714
-
715
- # Find task directory
716
- task_dir = find_task_by_name(task_name, tasks_dir)
717
-
718
- if not task_dir or not task_dir.is_dir():
719
- print(colored(f"Error: Task not found: {task_name}", Colors.RED), file=sys.stderr)
720
- print("Active tasks:", file=sys.stderr)
721
- cmd_list(argparse.Namespace(mine=False, status=None))
722
- return 1
723
-
724
- dir_name = task_dir.name
725
- task_json_path = task_dir / FILE_TASK_JSON
726
-
727
- # Update status before archiving
728
- today = datetime.now().strftime("%Y-%m-%d")
729
- if task_json_path.is_file():
730
- data = _read_json_file(task_json_path)
731
- if data:
732
- data["status"] = "completed"
733
- data["completedAt"] = today
734
- _write_json_file(task_json_path, data)
735
-
736
- # Handle subtask relationships on archive
737
- task_parent = data.get("parent")
738
- task_children = data.get("children", [])
739
-
740
- # If this is a child, remove from parent's children list
741
- if task_parent:
742
- parent_dir = find_task_by_name(task_parent, tasks_dir)
743
- if parent_dir:
744
- parent_json = parent_dir / FILE_TASK_JSON
745
- if parent_json.is_file():
746
- parent_data = _read_json_file(parent_json)
747
- if parent_data:
748
- parent_children = parent_data.get("children", [])
749
- if dir_name in parent_children:
750
- parent_children.remove(dir_name)
751
- parent_data["children"] = parent_children
752
- _write_json_file(parent_json, parent_data)
753
-
754
- # If this is a parent, clear parent field in all children
755
- if task_children:
756
- for child_name in task_children:
757
- child_dir_path = find_task_by_name(child_name, tasks_dir)
758
- if child_dir_path:
759
- child_json = child_dir_path / FILE_TASK_JSON
760
- if child_json.is_file():
761
- child_data = _read_json_file(child_json)
762
- if child_data:
763
- child_data["parent"] = None
764
- _write_json_file(child_json, child_data)
765
-
766
- # Clear if current task
767
- current = get_current_task(repo_root)
768
- if current and dir_name in current:
769
- clear_current_task(repo_root)
770
-
771
- # Archive
772
- result = archive_task_complete(task_dir, repo_root)
773
- if "archived_to" in result:
774
- archive_dest = Path(result["archived_to"])
775
- year_month = archive_dest.parent.name
776
- print(colored(f"Archived: {dir_name} -> archive/{year_month}/", Colors.GREEN), file=sys.stderr)
777
-
778
- # Auto-commit unless --no-commit
779
- if not getattr(args, "no_commit", False):
780
- _auto_commit_archive(dir_name, repo_root)
781
-
782
- # Return the archive path
783
- print(f"{DIR_WORKFLOW}/{DIR_TASKS}/{DIR_ARCHIVE}/{year_month}/{dir_name}")
784
-
785
- # Run hooks with the archived path
786
- archived_json = archive_dest / FILE_TASK_JSON
787
- _run_hooks("after_archive", archived_json, repo_root)
788
- return 0
789
-
790
- return 1
791
-
792
-
793
- def _auto_commit_archive(task_name: str, repo_root: Path) -> None:
794
- """Stage .trellis/tasks/ changes and commit after archive."""
795
- tasks_rel = f"{DIR_WORKFLOW}/{DIR_TASKS}"
796
- _run_git_command(["add", "-A", tasks_rel], cwd=repo_root)
797
-
798
- # Check if there are staged changes
799
- rc, _, _ = _run_git_command(
800
- ["diff", "--cached", "--quiet", "--", tasks_rel], cwd=repo_root
801
- )
802
- if rc == 0:
803
- print("[OK] No task changes to commit.", file=sys.stderr)
804
- return
805
-
806
- commit_msg = f"chore(task): archive {task_name}"
807
- rc, _, err = _run_git_command(["commit", "-m", commit_msg], cwd=repo_root)
808
- if rc == 0:
809
- print(f"[OK] Auto-committed: {commit_msg}", file=sys.stderr)
810
- else:
811
- print(f"[WARN] Auto-commit failed: {err.strip()}", file=sys.stderr)
812
-
813
-
814
- # =============================================================================
815
- # Command: add-subtask
816
- # =============================================================================
817
-
818
- def cmd_add_subtask(args: argparse.Namespace) -> int:
819
- """Link a child task to a parent task."""
820
- repo_root = get_repo_root()
821
-
822
- parent_dir = _resolve_task_dir(args.parent_dir, repo_root)
823
- child_dir = _resolve_task_dir(args.child_dir, repo_root)
824
-
825
- parent_json_path = parent_dir / FILE_TASK_JSON
826
- child_json_path = child_dir / FILE_TASK_JSON
827
-
828
- if not parent_json_path.is_file():
829
- print(colored(f"Error: Parent task.json not found: {args.parent_dir}", Colors.RED), file=sys.stderr)
830
- return 1
831
-
832
- if not child_json_path.is_file():
833
- print(colored(f"Error: Child task.json not found: {args.child_dir}", Colors.RED), file=sys.stderr)
834
- return 1
835
-
836
- parent_data = _read_json_file(parent_json_path)
837
- child_data = _read_json_file(child_json_path)
838
-
839
- if not parent_data or not child_data:
840
- print(colored("Error: Failed to read task.json", Colors.RED), file=sys.stderr)
841
- return 1
842
-
843
- # Check if child already has a parent
844
- existing_parent = child_data.get("parent")
845
- if existing_parent:
846
- print(colored(f"Error: Child task already has a parent: {existing_parent}", Colors.RED), file=sys.stderr)
847
- return 1
848
-
849
- # Add child to parent's children list
850
- parent_children = parent_data.get("children", [])
851
- child_dir_name = child_dir.name
852
- if child_dir_name not in parent_children:
853
- parent_children.append(child_dir_name)
854
- parent_data["children"] = parent_children
855
-
856
- # Set parent in child's task.json
857
- child_data["parent"] = parent_dir.name
858
-
859
- # Write both
860
- _write_json_file(parent_json_path, parent_data)
861
- _write_json_file(child_json_path, child_data)
862
-
863
- print(colored(f"Linked: {child_dir.name} -> {parent_dir.name}", Colors.GREEN), file=sys.stderr)
864
- return 0
865
-
866
-
867
- # =============================================================================
868
- # Command: remove-subtask
869
- # =============================================================================
870
-
871
- def cmd_remove_subtask(args: argparse.Namespace) -> int:
872
- """Unlink a child task from a parent task."""
873
- repo_root = get_repo_root()
874
-
875
- parent_dir = _resolve_task_dir(args.parent_dir, repo_root)
876
- child_dir = _resolve_task_dir(args.child_dir, repo_root)
877
-
878
- parent_json_path = parent_dir / FILE_TASK_JSON
879
- child_json_path = child_dir / FILE_TASK_JSON
880
-
881
- if not parent_json_path.is_file():
882
- print(colored(f"Error: Parent task.json not found: {args.parent_dir}", Colors.RED), file=sys.stderr)
883
- return 1
884
-
885
- if not child_json_path.is_file():
886
- print(colored(f"Error: Child task.json not found: {args.child_dir}", Colors.RED), file=sys.stderr)
887
- return 1
888
-
889
- parent_data = _read_json_file(parent_json_path)
890
- child_data = _read_json_file(child_json_path)
891
-
892
- if not parent_data or not child_data:
893
- print(colored("Error: Failed to read task.json", Colors.RED), file=sys.stderr)
894
- return 1
895
-
896
- # Remove child from parent's children list
897
- parent_children = parent_data.get("children", [])
898
- child_dir_name = child_dir.name
899
- if child_dir_name in parent_children:
900
- parent_children.remove(child_dir_name)
901
- parent_data["children"] = parent_children
902
-
903
- # Clear parent in child's task.json
904
- child_data["parent"] = None
905
-
906
- # Write both
907
- _write_json_file(parent_json_path, parent_data)
908
- _write_json_file(child_json_path, child_data)
909
-
910
- print(colored(f"Unlinked: {child_dir.name} from {parent_dir.name}", Colors.GREEN), file=sys.stderr)
120
+ run_task_hooks("after_finish", task_json_path, repo_root)
911
121
  return 0
912
122
 
913
123
 
@@ -915,24 +125,6 @@ def cmd_remove_subtask(args: argparse.Namespace) -> int:
915
125
  # Command: list
916
126
  # =============================================================================
917
127
 
918
- def _get_children_progress(children: list[str], tasks_dir: Path) -> str:
919
- """Get children progress summary like '[2/3 done]'."""
920
- if not children:
921
- return ""
922
- done_count = 0
923
- total = len(children)
924
- for child_name in children:
925
- child_dir = tasks_dir / child_name
926
- child_json = child_dir / FILE_TASK_JSON
927
- if child_json.is_file():
928
- data = _read_json_file(child_json)
929
- if data:
930
- status = data.get("status", "")
931
- if status in ("completed", "done"):
932
- done_count += 1
933
- return f" [{done_count}/{total} done]"
934
-
935
-
936
128
  def cmd_list(args: argparse.Namespace) -> int:
937
129
  """List active tasks."""
938
130
  repo_root = get_repo_root()
@@ -951,51 +143,23 @@ def cmd_list(args: argparse.Namespace) -> int:
951
143
  print(colored("All active tasks:", Colors.BLUE))
952
144
  print()
953
145
 
954
- # First pass: collect all task data and identify parent/child relationships
955
- all_tasks: dict[str, dict] = {}
956
- if tasks_dir.is_dir():
957
- for d in sorted(tasks_dir.iterdir()):
958
- if not d.is_dir() or d.name == "archive":
959
- continue
960
-
961
- dir_name = d.name
962
- task_json = d / FILE_TASK_JSON
963
- status = "unknown"
964
- assignee = "-"
965
- children: list[str] = []
966
- parent: str | None = None
967
-
968
- if task_json.is_file():
969
- data = _read_json_file(task_json)
970
- if data:
971
- status = data.get("status", "unknown")
972
- assignee = data.get("assignee", "-")
973
- children = data.get("children", [])
974
- parent = data.get("parent")
975
-
976
- all_tasks[dir_name] = {
977
- "status": status,
978
- "assignee": assignee,
979
- "children": children,
980
- "parent": parent,
981
- }
982
-
983
- # Second pass: display tasks hierarchically
146
+ # Single pass: collect all tasks via shared iterator
147
+ all_tasks = {t.dir_name: t for t in iter_active_tasks(tasks_dir)}
148
+ all_statuses = {name: t.status for name, t in all_tasks.items()}
149
+
150
+ # Display tasks hierarchically
984
151
  count = 0
985
152
 
986
153
  def _print_task(dir_name: str, indent: int = 0) -> None:
987
154
  nonlocal count
988
- info = all_tasks[dir_name]
989
- status = info["status"]
990
- assignee = info["assignee"]
991
- children = info["children"]
155
+ t = all_tasks[dir_name]
992
156
 
993
157
  # Apply --mine filter
994
- if filter_mine and assignee != developer:
158
+ if filter_mine and (t.assignee or "-") != developer:
995
159
  return
996
160
 
997
161
  # Apply --status filter
998
- if filter_status and status != filter_status:
162
+ if filter_status and t.status != filter_status:
999
163
  return
1000
164
 
1001
165
  relative_path = f"{DIR_WORKFLOW}/{DIR_TASKS}/{dir_name}"
@@ -1004,25 +168,27 @@ def cmd_list(args: argparse.Namespace) -> int:
1004
168
  marker = f" {colored('<- current', Colors.GREEN)}"
1005
169
 
1006
170
  # Children progress
1007
- progress = _get_children_progress(children, tasks_dir) if children else ""
171
+ progress = children_progress(t.children, all_statuses)
172
+
173
+ # Package tag
174
+ pkg_tag = f" @{t.package}" if t.package else ""
1008
175
 
1009
176
  prefix = " " * indent + " - "
1010
177
 
1011
178
  if filter_mine:
1012
- print(f"{prefix}{dir_name}/ ({status}){progress}{marker}")
179
+ print(f"{prefix}{dir_name}/ ({t.status}){pkg_tag}{progress}{marker}")
1013
180
  else:
1014
- print(f"{prefix}{dir_name}/ ({status}){progress} [{colored(assignee, Colors.CYAN)}]{marker}")
181
+ print(f"{prefix}{dir_name}/ ({t.status}){pkg_tag}{progress} [{colored(t.assignee or '-', Colors.CYAN)}]{marker}")
1015
182
  count += 1
1016
183
 
1017
184
  # Print children indented
1018
- for child_name in children:
185
+ for child_name in t.children:
1019
186
  if child_name in all_tasks:
1020
187
  _print_task(child_name, indent + 1)
1021
188
 
1022
189
  # Display only top-level tasks (those without a parent)
1023
190
  for dir_name in sorted(all_tasks.keys()):
1024
- info = all_tasks[dir_name]
1025
- if not info["parent"]:
191
+ if not all_tasks[dir_name].parent:
1026
192
  _print_task(dir_name)
1027
193
 
1028
194
  if count == 0:
@@ -1070,106 +236,6 @@ def cmd_list_archive(args: argparse.Namespace) -> int:
1070
236
  return 0
1071
237
 
1072
238
 
1073
- # =============================================================================
1074
- # Command: set-branch
1075
- # =============================================================================
1076
-
1077
- def cmd_set_branch(args: argparse.Namespace) -> int:
1078
- """Set git branch for task."""
1079
- repo_root = get_repo_root()
1080
- target_dir = _resolve_task_dir(args.dir, repo_root)
1081
- branch = args.branch
1082
-
1083
- if not branch:
1084
- print(colored("Error: Missing arguments", Colors.RED))
1085
- print("Usage: python3 task.py set-branch <task-dir> <branch-name>")
1086
- return 1
1087
-
1088
- task_json = target_dir / FILE_TASK_JSON
1089
- if not task_json.is_file():
1090
- print(colored(f"Error: task.json not found at {target_dir}", Colors.RED))
1091
- return 1
1092
-
1093
- data = _read_json_file(task_json)
1094
- if not data:
1095
- return 1
1096
-
1097
- data["branch"] = branch
1098
- _write_json_file(task_json, data)
1099
-
1100
- print(colored(f"✓ Branch set to: {branch}", Colors.GREEN))
1101
- print()
1102
- print(colored("Now you can start the multi-agent pipeline:", Colors.BLUE))
1103
- print(f" python3 ./.trellis/scripts/multi_agent/start.py {args.dir}")
1104
- return 0
1105
-
1106
-
1107
- # =============================================================================
1108
- # Command: set-base-branch
1109
- # =============================================================================
1110
-
1111
- def cmd_set_base_branch(args: argparse.Namespace) -> int:
1112
- """Set the base branch (PR target) for task."""
1113
- repo_root = get_repo_root()
1114
- target_dir = _resolve_task_dir(args.dir, repo_root)
1115
- base_branch = args.base_branch
1116
-
1117
- if not base_branch:
1118
- print(colored("Error: Missing arguments", Colors.RED))
1119
- print("Usage: python3 task.py set-base-branch <task-dir> <base-branch>")
1120
- print("Example: python3 task.py set-base-branch <dir> develop")
1121
- print()
1122
- print("This sets the target branch for PR (the branch your feature will merge into).")
1123
- return 1
1124
-
1125
- task_json = target_dir / FILE_TASK_JSON
1126
- if not task_json.is_file():
1127
- print(colored(f"Error: task.json not found at {target_dir}", Colors.RED))
1128
- return 1
1129
-
1130
- data = _read_json_file(task_json)
1131
- if not data:
1132
- return 1
1133
-
1134
- data["base_branch"] = base_branch
1135
- _write_json_file(task_json, data)
1136
-
1137
- print(colored(f"✓ Base branch set to: {base_branch}", Colors.GREEN))
1138
- print(f" PR will target: {base_branch}")
1139
- return 0
1140
-
1141
-
1142
- # =============================================================================
1143
- # Command: set-scope
1144
- # =============================================================================
1145
-
1146
- def cmd_set_scope(args: argparse.Namespace) -> int:
1147
- """Set scope for PR title."""
1148
- repo_root = get_repo_root()
1149
- target_dir = _resolve_task_dir(args.dir, repo_root)
1150
- scope = args.scope
1151
-
1152
- if not scope:
1153
- print(colored("Error: Missing arguments", Colors.RED))
1154
- print("Usage: python3 task.py set-scope <task-dir> <scope>")
1155
- return 1
1156
-
1157
- task_json = target_dir / FILE_TASK_JSON
1158
- if not task_json.is_file():
1159
- print(colored(f"Error: task.json not found at {target_dir}", Colors.RED))
1160
- return 1
1161
-
1162
- data = _read_json_file(task_json)
1163
- if not data:
1164
- return 1
1165
-
1166
- data["scope"] = scope
1167
- _write_json_file(task_json, data)
1168
-
1169
- print(colored(f"✓ Scope set to: {scope}", Colors.GREEN))
1170
- return 0
1171
-
1172
-
1173
239
  # =============================================================================
1174
240
  # Command: create-pr (delegates to multi-agent script)
1175
241
  # =============================================================================
@@ -1200,8 +266,10 @@ def show_usage() -> None:
1200
266
 
1201
267
  Usage:
1202
268
  python3 task.py create <title> Create new task directory
269
+ python3 task.py create <title> --package <pkg> Create task for a specific package
1203
270
  python3 task.py create <title> --parent <dir> Create task as child of parent
1204
271
  python3 task.py init-context <dir> <dev_type> Initialize jsonl files
272
+ python3 task.py init-context <dir> <type> --package <pkg> With explicit package
1205
273
  python3 task.py add-context <dir> <jsonl> <path> [reason] Add entry to jsonl
1206
274
  python3 task.py validate <dir> Validate jsonl files
1207
275
  python3 task.py list-context <dir> List jsonl entries
@@ -1219,15 +287,20 @@ Usage:
1219
287
  Arguments:
1220
288
  dev_type: backend | frontend | fullstack | test | docs
1221
289
 
290
+ Monorepo options:
291
+ --package <pkg> Package name (validated against config.yaml packages)
292
+
1222
293
  List options:
1223
294
  --mine, -m Show only tasks assigned to current developer
1224
295
  --status, -s <s> Filter by status (planning, in_progress, review, completed)
1225
296
 
1226
297
  Examples:
1227
298
  python3 task.py create "Add login feature" --slug add-login
299
+ python3 task.py create "Add login feature" --slug add-login --package cli
1228
300
  python3 task.py create "Child task" --slug child --parent .trellis/tasks/01-21-parent
1229
301
  python3 task.py init-context .trellis/tasks/01-21-add-login backend
1230
- python3 task.py add-context <dir> implement .trellis/spec/backend/auth.md "Auth guidelines"
302
+ python3 task.py init-context .trellis/tasks/01-21-add-login backend --package cli
303
+ python3 task.py add-context <dir> implement .trellis/spec/cli/backend/auth.md "Auth guidelines"
1231
304
  python3 task.py set-branch <dir> task/add-login
1232
305
  python3 task.py start .trellis/tasks/01-21-add-login
1233
306
  python3 task.py create-pr # Uses current task
@@ -1262,11 +335,13 @@ def main() -> int:
1262
335
  p_create.add_argument("--priority", "-p", default="P2", help="Priority (P0-P3)")
1263
336
  p_create.add_argument("--description", "-d", help="Task description")
1264
337
  p_create.add_argument("--parent", help="Parent task directory (establishes subtask link)")
338
+ p_create.add_argument("--package", help="Package name for monorepo projects")
1265
339
 
1266
340
  # init-context
1267
341
  p_init = subparsers.add_parser("init-context", help="Initialize context files")
1268
342
  p_init.add_argument("dir", help="Task directory")
1269
343
  p_init.add_argument("type", help="Dev type: backend|frontend|fullstack|test|docs")
344
+ p_init.add_argument("--package", help="Package name for monorepo projects")
1270
345
 
1271
346
  # add-context
1272
347
  p_add = subparsers.add_parser("add-context", help="Add context entry")