@mindfoldhq/trellis 0.3.10-beta.0 → 0.3.10

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 (173) hide show
  1. package/dist/cli/index.js +0 -2
  2. package/dist/cli/index.js.map +1 -1
  3. package/dist/commands/init.d.ts +0 -1
  4. package/dist/commands/init.d.ts.map +1 -1
  5. package/dist/commands/init.js +31 -203
  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 +6 -154
  9. package/dist/commands/update.js.map +1 -1
  10. package/dist/configurators/workflow.d.ts +2 -6
  11. package/dist/configurators/workflow.d.ts.map +1 -1
  12. package/dist/configurators/workflow.js +58 -88
  13. package/dist/configurators/workflow.js.map +1 -1
  14. package/dist/migrations/index.d.ts +0 -1
  15. package/dist/migrations/index.d.ts.map +1 -1
  16. package/dist/migrations/index.js +0 -2
  17. package/dist/migrations/index.js.map +1 -1
  18. package/dist/migrations/manifests/0.3.10.json +9 -0
  19. package/dist/templates/claude/agents/dispatch.md +2 -1
  20. package/dist/templates/claude/agents/implement.md +3 -2
  21. package/dist/templates/claude/commands/trellis/before-backend-dev.md +13 -0
  22. package/dist/templates/claude/commands/trellis/before-frontend-dev.md +13 -0
  23. package/dist/templates/claude/commands/trellis/check-backend.md +13 -0
  24. package/dist/templates/claude/commands/trellis/check-frontend.md +13 -0
  25. package/dist/templates/claude/commands/trellis/create-command.md +2 -2
  26. package/dist/templates/claude/commands/trellis/onboard.md +13 -13
  27. package/dist/templates/claude/commands/trellis/parallel.md +2 -1
  28. package/dist/templates/claude/commands/trellis/record-session.md +2 -2
  29. package/dist/templates/claude/commands/trellis/start.md +4 -8
  30. package/dist/templates/claude/hooks/inject-subagent-context.py +13 -21
  31. package/dist/templates/claude/hooks/session-start.py +2 -170
  32. package/dist/templates/codex/skills/before-backend-dev/SKILL.md +18 -0
  33. package/dist/templates/codex/skills/before-frontend-dev/SKILL.md +18 -0
  34. package/dist/templates/codex/skills/check-backend/SKILL.md +18 -0
  35. package/dist/templates/codex/skills/check-frontend/SKILL.md +18 -0
  36. package/dist/templates/codex/skills/create-command/SKILL.md +2 -2
  37. package/dist/templates/codex/skills/onboard/SKILL.md +11 -11
  38. package/dist/templates/codex/skills/record-session/SKILL.md +2 -2
  39. package/dist/templates/codex/skills/start/SKILL.md +3 -8
  40. package/dist/templates/cursor/commands/trellis-before-backend-dev.md +13 -0
  41. package/dist/templates/cursor/commands/trellis-before-frontend-dev.md +13 -0
  42. package/dist/templates/cursor/commands/trellis-check-backend.md +13 -0
  43. package/dist/templates/cursor/commands/trellis-check-frontend.md +13 -0
  44. package/dist/templates/cursor/commands/trellis-create-command.md +2 -2
  45. package/dist/templates/cursor/commands/trellis-onboard.md +13 -13
  46. package/dist/templates/cursor/commands/trellis-record-session.md +2 -2
  47. package/dist/templates/cursor/commands/trellis-start.md +16 -7
  48. package/dist/templates/gemini/commands/trellis/before-backend-dev.toml +17 -0
  49. package/dist/templates/gemini/commands/trellis/before-frontend-dev.toml +17 -0
  50. package/dist/templates/gemini/commands/trellis/check-backend.toml +17 -0
  51. package/dist/templates/gemini/commands/trellis/check-frontend.toml +17 -0
  52. package/dist/templates/gemini/commands/trellis/create-command.toml +2 -2
  53. package/dist/templates/gemini/commands/trellis/onboard.toml +2 -2
  54. package/dist/templates/gemini/commands/trellis/record-session.toml +2 -2
  55. package/dist/templates/gemini/commands/trellis/start.toml +4 -9
  56. package/dist/templates/iflow/agents/dispatch.md +2 -1
  57. package/dist/templates/iflow/agents/implement.md +3 -2
  58. package/dist/templates/iflow/commands/trellis/before-backend-dev.md +13 -0
  59. package/dist/templates/iflow/commands/trellis/before-frontend-dev.md +13 -0
  60. package/dist/templates/iflow/commands/trellis/check-backend.md +13 -0
  61. package/dist/templates/iflow/commands/trellis/check-frontend.md +13 -0
  62. package/dist/templates/iflow/commands/trellis/create-command.md +2 -2
  63. package/dist/templates/iflow/commands/trellis/onboard.md +13 -13
  64. package/dist/templates/iflow/commands/trellis/parallel.md +2 -1
  65. package/dist/templates/iflow/commands/trellis/record-session.md +2 -2
  66. package/dist/templates/iflow/commands/trellis/start.md +4 -8
  67. package/dist/templates/iflow/hooks/inject-subagent-context.py +13 -21
  68. package/dist/templates/iflow/hooks/session-start.py +1 -156
  69. package/dist/templates/kilo/workflows/before-backend-dev.md +13 -0
  70. package/dist/templates/kilo/workflows/before-frontend-dev.md +13 -0
  71. package/dist/templates/kilo/workflows/check-backend.md +13 -0
  72. package/dist/templates/kilo/workflows/check-frontend.md +13 -0
  73. package/dist/templates/kilo/workflows/create-command.md +2 -2
  74. package/dist/templates/kilo/workflows/onboard.md +13 -13
  75. package/dist/templates/kilo/workflows/parallel.md +2 -1
  76. package/dist/templates/kilo/workflows/record-session.md +2 -2
  77. package/dist/templates/kilo/workflows/start.md +3 -8
  78. package/dist/templates/kiro/skills/before-backend-dev/SKILL.md +18 -0
  79. package/dist/templates/kiro/skills/before-frontend-dev/SKILL.md +18 -0
  80. package/dist/templates/kiro/skills/check-backend/SKILL.md +18 -0
  81. package/dist/templates/kiro/skills/check-frontend/SKILL.md +18 -0
  82. package/dist/templates/kiro/skills/create-command/SKILL.md +2 -2
  83. package/dist/templates/kiro/skills/onboard/SKILL.md +11 -11
  84. package/dist/templates/kiro/skills/record-session/SKILL.md +2 -2
  85. package/dist/templates/kiro/skills/start/SKILL.md +3 -8
  86. package/dist/templates/markdown/spec/backend/script-conventions.md +0 -93
  87. package/dist/templates/opencode/agents/dispatch.md +2 -1
  88. package/dist/templates/opencode/agents/implement.md +2 -2
  89. package/dist/templates/opencode/agents/research.md +2 -1
  90. package/dist/templates/opencode/commands/trellis/before-backend-dev.md +13 -0
  91. package/dist/templates/opencode/commands/trellis/before-frontend-dev.md +13 -0
  92. package/dist/templates/opencode/commands/trellis/check-backend.md +13 -0
  93. package/dist/templates/opencode/commands/trellis/check-frontend.md +13 -0
  94. package/dist/templates/opencode/commands/trellis/create-command.md +2 -2
  95. package/dist/templates/opencode/commands/trellis/onboard.md +13 -13
  96. package/dist/templates/opencode/commands/trellis/parallel.md +2 -1
  97. package/dist/templates/opencode/commands/trellis/record-session.md +2 -2
  98. package/dist/templates/opencode/commands/trellis/start.md +3 -8
  99. package/dist/templates/opencode/plugin/inject-subagent-context.js +18 -45
  100. package/dist/templates/opencode/plugin/session-start.js +1 -149
  101. package/dist/templates/qoder/skills/before-backend-dev/SKILL.md +18 -0
  102. package/dist/templates/qoder/skills/before-frontend-dev/SKILL.md +18 -0
  103. package/dist/templates/qoder/skills/check-backend/SKILL.md +18 -0
  104. package/dist/templates/qoder/skills/check-frontend/SKILL.md +18 -0
  105. package/dist/templates/qoder/skills/create-command/SKILL.md +2 -2
  106. package/dist/templates/qoder/skills/onboard/SKILL.md +13 -13
  107. package/dist/templates/qoder/skills/record-session/SKILL.md +2 -2
  108. package/dist/templates/qoder/skills/start/SKILL.md +3 -8
  109. package/dist/templates/trellis/config.yaml +0 -20
  110. package/dist/templates/trellis/index.d.ts +0 -11
  111. package/dist/templates/trellis/index.d.ts.map +1 -1
  112. package/dist/templates/trellis/index.js +0 -22
  113. package/dist/templates/trellis/index.js.map +1 -1
  114. package/dist/templates/trellis/scripts/add_session.py +7 -52
  115. package/dist/templates/trellis/scripts/common/cli_adapter.py +45 -33
  116. package/dist/templates/trellis/scripts/common/config.py +0 -152
  117. package/dist/templates/trellis/scripts/common/git_context.py +586 -23
  118. package/dist/templates/trellis/scripts/common/paths.py +0 -46
  119. package/dist/templates/trellis/scripts/common/phase.py +49 -50
  120. package/dist/templates/trellis/scripts/common/registry.py +72 -41
  121. package/dist/templates/trellis/scripts/common/task_queue.py +98 -27
  122. package/dist/templates/trellis/scripts/common/task_utils.py +6 -96
  123. package/dist/templates/trellis/scripts/create_bootstrap.py +26 -31
  124. package/dist/templates/trellis/scripts/multi_agent/cleanup.py +48 -43
  125. package/dist/templates/trellis/scripts/multi_agent/create_pr.py +45 -336
  126. package/dist/templates/trellis/scripts/multi_agent/plan.py +26 -2
  127. package/dist/templates/trellis/scripts/multi_agent/start.py +57 -126
  128. package/dist/templates/trellis/scripts/multi_agent/status.py +753 -12
  129. package/dist/templates/trellis/scripts/task.py +975 -50
  130. package/dist/templates/trellis/workflow.md +34 -21
  131. package/dist/types/migration.d.ts +1 -3
  132. package/dist/types/migration.d.ts.map +1 -1
  133. package/dist/utils/project-detector.d.ts +0 -23
  134. package/dist/utils/project-detector.d.ts.map +1 -1
  135. package/dist/utils/project-detector.js +0 -364
  136. package/dist/utils/project-detector.js.map +1 -1
  137. package/dist/utils/template-fetcher.d.ts +10 -2
  138. package/dist/utils/template-fetcher.d.ts.map +1 -1
  139. package/dist/utils/template-fetcher.js +43 -12
  140. package/dist/utils/template-fetcher.js.map +1 -1
  141. package/package.json +1 -1
  142. package/dist/migrations/manifests/0.4.0-beta.1.json +0 -228
  143. package/dist/templates/claude/commands/trellis/before-dev.md +0 -29
  144. package/dist/templates/claude/commands/trellis/check.md +0 -25
  145. package/dist/templates/codex/skills/before-dev/SKILL.md +0 -34
  146. package/dist/templates/codex/skills/check/SKILL.md +0 -30
  147. package/dist/templates/cursor/commands/trellis-before-dev.md +0 -29
  148. package/dist/templates/cursor/commands/trellis-check.md +0 -25
  149. package/dist/templates/gemini/commands/trellis/before-dev.toml +0 -33
  150. package/dist/templates/gemini/commands/trellis/check.toml +0 -29
  151. package/dist/templates/iflow/commands/trellis/before-dev.md +0 -29
  152. package/dist/templates/iflow/commands/trellis/check.md +0 -25
  153. package/dist/templates/kilo/workflows/before-dev.md +0 -29
  154. package/dist/templates/kilo/workflows/check.md +0 -25
  155. package/dist/templates/kiro/skills/before-dev/SKILL.md +0 -34
  156. package/dist/templates/kiro/skills/check/SKILL.md +0 -30
  157. package/dist/templates/opencode/commands/trellis/before-dev.md +0 -29
  158. package/dist/templates/opencode/commands/trellis/check.md +0 -25
  159. package/dist/templates/qoder/skills/before-dev/SKILL.md +0 -34
  160. package/dist/templates/qoder/skills/check/SKILL.md +0 -30
  161. package/dist/templates/trellis/scripts/common/git.py +0 -31
  162. package/dist/templates/trellis/scripts/common/io.py +0 -37
  163. package/dist/templates/trellis/scripts/common/log.py +0 -45
  164. package/dist/templates/trellis/scripts/common/packages_context.py +0 -233
  165. package/dist/templates/trellis/scripts/common/session_context.py +0 -466
  166. package/dist/templates/trellis/scripts/common/task_context.py +0 -384
  167. package/dist/templates/trellis/scripts/common/task_store.py +0 -534
  168. package/dist/templates/trellis/scripts/common/tasks.py +0 -109
  169. package/dist/templates/trellis/scripts/common/types.py +0 -112
  170. package/dist/templates/trellis/scripts/hooks/linear_sync.py +0 -243
  171. package/dist/templates/trellis/scripts/multi_agent/_bootstrap.py +0 -17
  172. package/dist/templates/trellis/scripts/multi_agent/status_display.py +0 -542
  173. package/dist/templates/trellis/scripts/multi_agent/status_monitor.py +0 -225
@@ -19,39 +19,25 @@ Provides:
19
19
 
20
20
  from __future__ import annotations
21
21
 
22
+ import json
22
23
  from pathlib import Path
23
24
 
24
- from .io import read_json, write_json
25
25
 
26
-
27
- # =============================================================================
28
- # Internal Helpers (operate on pre-loaded data dict)
29
- # =============================================================================
30
-
31
- def _total_phases(data: dict) -> int:
32
- """Get total phases from pre-loaded data."""
33
- next_action = data.get("next_action", [])
34
- return len(next_action) if isinstance(next_action, list) else 0
26
+ def _read_json_file(path: Path) -> dict | None:
27
+ """Read and parse a JSON file."""
28
+ try:
29
+ return json.loads(path.read_text(encoding="utf-8"))
30
+ except (FileNotFoundError, json.JSONDecodeError, OSError):
31
+ return None
35
32
 
36
33
 
37
- def _phase_action(data: dict, phase: int) -> str:
38
- """Get action name for a phase from pre-loaded data."""
39
- next_action = data.get("next_action", [])
40
- if isinstance(next_action, list):
41
- for item in next_action:
42
- if isinstance(item, dict) and item.get("phase") == phase:
43
- return item.get("action", "unknown")
44
- return "unknown"
45
-
46
-
47
- def _phase_for_action(data: dict, action: str) -> int:
48
- """Get phase number for an action name from pre-loaded data."""
49
- next_action = data.get("next_action", [])
50
- if isinstance(next_action, list):
51
- for item in next_action:
52
- if isinstance(item, dict) and item.get("action") == action:
53
- return item.get("phase", 0)
54
- return 0
34
+ def _write_json_file(path: Path, data: dict) -> bool:
35
+ """Write dict to JSON file."""
36
+ try:
37
+ path.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")
38
+ return True
39
+ except (OSError, IOError):
40
+ return False
55
41
 
56
42
 
57
43
  # =============================================================================
@@ -67,7 +53,7 @@ def get_current_phase(task_json: Path) -> int:
67
53
  Returns:
68
54
  Current phase number, or 0 if not found.
69
55
  """
70
- data = read_json(task_json)
56
+ data = _read_json_file(task_json)
71
57
  if not data:
72
58
  return 0
73
59
  return data.get("current_phase", 0) or 0
@@ -82,10 +68,14 @@ def get_total_phases(task_json: Path) -> int:
82
68
  Returns:
83
69
  Total phase count, or 0 if not found.
84
70
  """
85
- data = read_json(task_json)
71
+ data = _read_json_file(task_json)
86
72
  if not data:
87
73
  return 0
88
- return _total_phases(data)
74
+
75
+ next_action = data.get("next_action", [])
76
+ if isinstance(next_action, list):
77
+ return len(next_action)
78
+ return 0
89
79
 
90
80
 
91
81
  def get_phase_action(task_json: Path, phase: int) -> str:
@@ -98,10 +88,16 @@ def get_phase_action(task_json: Path, phase: int) -> str:
98
88
  Returns:
99
89
  Action name, or "unknown" if not found.
100
90
  """
101
- data = read_json(task_json)
91
+ data = _read_json_file(task_json)
102
92
  if not data:
103
93
  return "unknown"
104
- return _phase_action(data, phase)
94
+
95
+ next_action = data.get("next_action", [])
96
+ if isinstance(next_action, list):
97
+ for item in next_action:
98
+ if isinstance(item, dict) and item.get("phase") == phase:
99
+ return item.get("action", "unknown")
100
+ return "unknown"
105
101
 
106
102
 
107
103
  def get_phase_info(task_json: Path) -> str:
@@ -113,18 +109,18 @@ def get_phase_info(task_json: Path) -> str:
113
109
  Returns:
114
110
  Formatted string like "1/4 (implement)".
115
111
  """
116
- data = read_json(task_json)
112
+ data = _read_json_file(task_json)
117
113
  if not data:
118
114
  return "N/A"
119
115
 
120
116
  current_phase = data.get("current_phase", 0) or 0
121
- total = _total_phases(data)
122
- action_name = _phase_action(data, current_phase)
117
+ total_phases = get_total_phases(task_json)
118
+ action_name = get_phase_action(task_json, current_phase)
123
119
 
124
120
  if current_phase == 0 or current_phase is None:
125
- return f"0/{total} (pending)"
121
+ return f"0/{total_phases} (pending)"
126
122
  else:
127
- return f"{current_phase}/{total} ({action_name})"
123
+ return f"{current_phase}/{total_phases} ({action_name})"
128
124
 
129
125
 
130
126
  def set_phase(task_json: Path, phase: int) -> bool:
@@ -137,12 +133,12 @@ def set_phase(task_json: Path, phase: int) -> bool:
137
133
  Returns:
138
134
  True on success, False on error.
139
135
  """
140
- data = read_json(task_json)
136
+ data = _read_json_file(task_json)
141
137
  if not data:
142
138
  return False
143
139
 
144
140
  data["current_phase"] = phase
145
- return write_json(task_json, data)
141
+ return _write_json_file(task_json, data)
146
142
 
147
143
 
148
144
  def advance_phase(task_json: Path) -> bool:
@@ -154,19 +150,19 @@ def advance_phase(task_json: Path) -> bool:
154
150
  Returns:
155
151
  True on success, False on error or at final phase.
156
152
  """
157
- data = read_json(task_json)
153
+ data = _read_json_file(task_json)
158
154
  if not data:
159
155
  return False
160
156
 
161
157
  current = data.get("current_phase", 0) or 0
162
- total = _total_phases(data)
158
+ total = get_total_phases(task_json)
163
159
  next_phase = current + 1
164
160
 
165
161
  if next_phase > total:
166
162
  return False # Already at final phase
167
163
 
168
164
  data["current_phase"] = next_phase
169
- return write_json(task_json, data)
165
+ return _write_json_file(task_json, data)
170
166
 
171
167
 
172
168
  def get_phase_for_action(task_json: Path, action: str) -> int:
@@ -179,10 +175,16 @@ def get_phase_for_action(task_json: Path, action: str) -> int:
179
175
  Returns:
180
176
  Phase number, or 0 if not found.
181
177
  """
182
- data = read_json(task_json)
178
+ data = _read_json_file(task_json)
183
179
  if not data:
184
180
  return 0
185
- return _phase_for_action(data, action)
181
+
182
+ next_action = data.get("next_action", [])
183
+ if isinstance(next_action, list):
184
+ for item in next_action:
185
+ if isinstance(item, dict) and item.get("action") == action:
186
+ return item.get("phase", 0)
187
+ return 0
186
188
 
187
189
 
188
190
  def map_subagent_to_action(subagent_type: str) -> str:
@@ -229,11 +231,8 @@ def is_current_action(task_json: Path, action: str) -> bool:
229
231
  Returns:
230
232
  True if current phase matches the action.
231
233
  """
232
- data = read_json(task_json)
233
- if not data:
234
- return False
235
- current = data.get("current_phase", 0) or 0
236
- action_phase = _phase_for_action(data, action)
234
+ current = get_current_phase(task_json)
235
+ action_phase = get_phase_for_action(task_json, action)
237
236
  return current == action_phase
238
237
 
239
238
 
@@ -16,35 +16,29 @@ Provides:
16
16
 
17
17
  from __future__ import annotations
18
18
 
19
+ import json
19
20
  from datetime import datetime
20
21
  from pathlib import Path
21
22
 
22
- from .io import read_json, write_json
23
23
  from .paths import get_repo_root
24
24
  from .worktree import get_agents_dir
25
25
 
26
26
 
27
- # =============================================================================
28
- # Internal Helpers
29
- # =============================================================================
30
-
31
- def _load_registry(
32
- repo_root: Path | None = None,
33
- ) -> tuple[Path | None, dict | None]:
34
- """Load registry file and data in one step.
35
-
36
- Returns:
37
- (registry_file_path, data_dict) — either may be None.
38
- """
39
- if repo_root is None:
40
- repo_root = get_repo_root()
27
+ def _read_json_file(path: Path) -> dict | None:
28
+ """Read and parse a JSON file."""
29
+ try:
30
+ return json.loads(path.read_text(encoding="utf-8"))
31
+ except (FileNotFoundError, json.JSONDecodeError, OSError):
32
+ return None
41
33
 
42
- registry_file = registry_get_file(repo_root)
43
- if not registry_file or not registry_file.is_file():
44
- return registry_file, None
45
34
 
46
- data = read_json(registry_file)
47
- return registry_file, data
35
+ def _write_json_file(path: Path, data: dict) -> bool:
36
+ """Write dict to JSON file."""
37
+ try:
38
+ path.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")
39
+ return True
40
+ except (OSError, IOError):
41
+ return False
48
42
 
49
43
 
50
44
  # =============================================================================
@@ -91,7 +85,7 @@ def _ensure_registry(repo_root: Path | None = None) -> Path | None:
91
85
  agents_dir.mkdir(parents=True, exist_ok=True)
92
86
 
93
87
  if not registry_file.exists():
94
- write_json(registry_file, {"agents": []})
88
+ _write_json_file(registry_file, {"agents": []})
95
89
 
96
90
  return registry_file
97
91
  except (OSError, IOError):
@@ -115,7 +109,14 @@ def registry_get_agent_by_id(
115
109
  Returns:
116
110
  Agent dict, or None if not found.
117
111
  """
118
- _, data = _load_registry(repo_root)
112
+ if repo_root is None:
113
+ repo_root = get_repo_root()
114
+
115
+ registry_file = registry_get_file(repo_root)
116
+ if not registry_file or not registry_file.is_file():
117
+ return None
118
+
119
+ data = _read_json_file(registry_file)
119
120
  if not data:
120
121
  return None
121
122
 
@@ -139,7 +140,14 @@ def registry_get_agent_by_worktree(
139
140
  Returns:
140
141
  Agent dict, or None if not found.
141
142
  """
142
- _, data = _load_registry(repo_root)
143
+ if repo_root is None:
144
+ repo_root = get_repo_root()
145
+
146
+ registry_file = registry_get_file(repo_root)
147
+ if not registry_file or not registry_file.is_file():
148
+ return None
149
+
150
+ data = _read_json_file(registry_file)
143
151
  if not data:
144
152
  return None
145
153
 
@@ -163,7 +171,14 @@ def registry_search_agent(
163
171
  Returns:
164
172
  First matching agent dict, or None if not found.
165
173
  """
166
- _, data = _load_registry(repo_root)
174
+ if repo_root is None:
175
+ repo_root = get_repo_root()
176
+
177
+ registry_file = registry_get_file(repo_root)
178
+ if not registry_file or not registry_file.is_file():
179
+ return None
180
+
181
+ data = _read_json_file(registry_file)
167
182
  if not data:
168
183
  return None
169
184
 
@@ -192,14 +207,9 @@ def registry_get_task_dir(
192
207
  Returns:
193
208
  Task directory path, or None if not found.
194
209
  """
195
- _, data = _load_registry(repo_root)
196
- if not data:
197
- return None
198
-
199
- for agent in data.get("agents", []):
200
- if agent.get("worktree_path") == worktree_path:
201
- return agent.get("task_dir")
202
-
210
+ agent = registry_get_agent_by_worktree(worktree_path, repo_root)
211
+ if agent:
212
+ return agent.get("task_dir")
203
213
  return None
204
214
 
205
215
 
@@ -217,14 +227,21 @@ def registry_remove_by_id(agent_id: str, repo_root: Path | None = None) -> bool:
217
227
  Returns:
218
228
  True on success.
219
229
  """
220
- registry_file, data = _load_registry(repo_root)
221
- if not registry_file or not data:
230
+ if repo_root is None:
231
+ repo_root = get_repo_root()
232
+
233
+ registry_file = registry_get_file(repo_root)
234
+ if not registry_file or not registry_file.is_file():
222
235
  return True # Nothing to remove
223
236
 
237
+ data = _read_json_file(registry_file)
238
+ if not data:
239
+ return True
240
+
224
241
  agents = data.get("agents", [])
225
242
  data["agents"] = [a for a in agents if a.get("id") != agent_id]
226
243
 
227
- return write_json(registry_file, data)
244
+ return _write_json_file(registry_file, data)
228
245
 
229
246
 
230
247
  def registry_remove_by_worktree(
@@ -240,14 +257,21 @@ def registry_remove_by_worktree(
240
257
  Returns:
241
258
  True on success.
242
259
  """
243
- registry_file, data = _load_registry(repo_root)
244
- if not registry_file or not data:
260
+ if repo_root is None:
261
+ repo_root = get_repo_root()
262
+
263
+ registry_file = registry_get_file(repo_root)
264
+ if not registry_file or not registry_file.is_file():
245
265
  return True # Nothing to remove
246
266
 
267
+ data = _read_json_file(registry_file)
268
+ if not data:
269
+ return True
270
+
247
271
  agents = data.get("agents", [])
248
272
  data["agents"] = [a for a in agents if a.get("worktree_path") != worktree_path]
249
273
 
250
- return write_json(registry_file, data)
274
+ return _write_json_file(registry_file, data)
251
275
 
252
276
 
253
277
  def registry_add_agent(
@@ -278,7 +302,7 @@ def registry_add_agent(
278
302
  if not registry_file:
279
303
  return False
280
304
 
281
- data = read_json(registry_file)
305
+ data = _read_json_file(registry_file)
282
306
  if not data:
283
307
  data = {"agents": []}
284
308
 
@@ -300,7 +324,7 @@ def registry_add_agent(
300
324
  agents.append(new_agent)
301
325
  data["agents"] = agents
302
326
 
303
- return write_json(registry_file, data)
327
+ return _write_json_file(registry_file, data)
304
328
 
305
329
 
306
330
  def registry_list_agents(repo_root: Path | None = None) -> list[dict]:
@@ -312,7 +336,14 @@ def registry_list_agents(repo_root: Path | None = None) -> list[dict]:
312
336
  Returns:
313
337
  List of agent dicts.
314
338
  """
315
- _, data = _load_registry(repo_root)
339
+ if repo_root is None:
340
+ repo_root = get_repo_root()
341
+
342
+ registry_file = registry_get_file(repo_root)
343
+ if not registry_file or not registry_file.is_file():
344
+ return []
345
+
346
+ data = _read_json_file(registry_file)
316
347
  if not data:
317
348
  return []
318
349
 
@@ -12,32 +12,23 @@ Provides:
12
12
 
13
13
  from __future__ import annotations
14
14
 
15
+ import json
15
16
  from pathlib import Path
16
17
 
17
18
  from .paths import (
19
+ FILE_TASK_JSON,
18
20
  get_repo_root,
19
21
  get_developer,
20
22
  get_tasks_dir,
21
23
  )
22
- from .tasks import iter_active_tasks
23
24
 
24
25
 
25
- # =============================================================================
26
- # Internal helper
27
- # =============================================================================
28
-
29
- def _task_to_dict(t) -> dict:
30
- """Convert TaskInfo to the dict format callers expect."""
31
- return {
32
- "priority": t.priority,
33
- "id": t.raw.get("id", ""),
34
- "title": t.title,
35
- "status": t.status,
36
- "assignee": t.assignee or "-",
37
- "dir": t.dir_name,
38
- "children": list(t.children),
39
- "parent": t.parent,
40
- }
26
+ def _read_json_file(path: Path) -> dict | None:
27
+ """Read and parse a JSON file."""
28
+ try:
29
+ return json.loads(path.read_text(encoding="utf-8"))
30
+ except (FileNotFoundError, json.JSONDecodeError, OSError):
31
+ return None
41
32
 
42
33
 
43
34
  # =============================================================================
@@ -63,10 +54,41 @@ def list_tasks_by_status(
63
54
  tasks_dir = get_tasks_dir(repo_root)
64
55
  results = []
65
56
 
66
- for t in iter_active_tasks(tasks_dir):
67
- if filter_status and t.status != filter_status:
57
+ if not tasks_dir.is_dir():
58
+ return results
59
+
60
+ for d in tasks_dir.iterdir():
61
+ if not d.is_dir() or d.name == "archive":
62
+ continue
63
+
64
+ task_json = d / FILE_TASK_JSON
65
+ if not task_json.is_file():
66
+ continue
67
+
68
+ data = _read_json_file(task_json)
69
+ if not data:
68
70
  continue
69
- results.append(_task_to_dict(t))
71
+
72
+ task_id = data.get("id", "")
73
+ title = data.get("title") or data.get("name", "")
74
+ priority = data.get("priority", "P2")
75
+ status = data.get("status", "planning")
76
+ assignee = data.get("assignee", "-")
77
+
78
+ # Apply filter
79
+ if filter_status and status != filter_status:
80
+ continue
81
+
82
+ results.append({
83
+ "priority": priority,
84
+ "id": task_id,
85
+ "title": title,
86
+ "status": status,
87
+ "assignee": assignee,
88
+ "dir": d.name,
89
+ "children": data.get("children", []),
90
+ "parent": data.get("parent"),
91
+ })
70
92
 
71
93
  return results
72
94
 
@@ -104,12 +126,46 @@ def list_tasks_by_assignee(
104
126
  tasks_dir = get_tasks_dir(repo_root)
105
127
  results = []
106
128
 
107
- for t in iter_active_tasks(tasks_dir):
108
- if (t.assignee or "-") != assignee:
129
+ if not tasks_dir.is_dir():
130
+ return results
131
+
132
+ for d in tasks_dir.iterdir():
133
+ if not d.is_dir() or d.name == "archive":
134
+ continue
135
+
136
+ task_json = d / FILE_TASK_JSON
137
+ if not task_json.is_file():
109
138
  continue
110
- if filter_status and t.status != filter_status:
139
+
140
+ data = _read_json_file(task_json)
141
+ if not data:
142
+ continue
143
+
144
+ task_assignee = data.get("assignee", "-")
145
+
146
+ # Apply assignee filter
147
+ if task_assignee != assignee:
111
148
  continue
112
- results.append(_task_to_dict(t))
149
+
150
+ task_id = data.get("id", "")
151
+ title = data.get("title") or data.get("name", "")
152
+ priority = data.get("priority", "P2")
153
+ status = data.get("status", "planning")
154
+
155
+ # Apply status filter
156
+ if filter_status and status != filter_status:
157
+ continue
158
+
159
+ results.append({
160
+ "priority": priority,
161
+ "id": task_id,
162
+ "title": title,
163
+ "status": status,
164
+ "assignee": task_assignee,
165
+ "dir": d.name,
166
+ "children": data.get("children", []),
167
+ "parent": data.get("parent"),
168
+ })
113
169
 
114
170
  return results
115
171
 
@@ -155,9 +211,24 @@ def get_task_stats(repo_root: Path | None = None) -> dict[str, int]:
155
211
  tasks_dir = get_tasks_dir(repo_root)
156
212
  stats = {"P0": 0, "P1": 0, "P2": 0, "P3": 0, "Total": 0}
157
213
 
158
- for t in iter_active_tasks(tasks_dir):
159
- if t.priority in stats:
160
- stats[t.priority] += 1
214
+ if not tasks_dir.is_dir():
215
+ return stats
216
+
217
+ for d in tasks_dir.iterdir():
218
+ if not d.is_dir() or d.name == "archive":
219
+ continue
220
+
221
+ task_json = d / FILE_TASK_JSON
222
+ if not task_json.is_file():
223
+ continue
224
+
225
+ data = _read_json_file(task_json)
226
+ if not data:
227
+ continue
228
+
229
+ priority = data.get("priority", "P2")
230
+ if priority in stats:
231
+ stats[priority] += 1
161
232
  stats["Total"] += 1
162
233
 
163
234
  return stats
@@ -3,11 +3,9 @@
3
3
  Task utility functions.
4
4
 
5
5
  Provides:
6
- is_safe_task_path - Validate task path is safe to operate on
7
- find_task_by_name - Find task directory by name
8
- resolve_task_dir - Resolve task directory from name, relative, or absolute path
9
- archive_task_dir - Archive task to monthly directory
10
- run_task_hooks - Run lifecycle hooks for task events
6
+ is_safe_task_path - Validate task path is safe to operate on
7
+ find_task_by_name - Find task directory by name
8
+ archive_task_dir - Archive task to monthly directory
11
9
  """
12
10
 
13
11
  from __future__ import annotations
@@ -17,7 +15,7 @@ import sys
17
15
  from datetime import datetime
18
16
  from pathlib import Path
19
17
 
20
- from .paths import get_repo_root, get_tasks_dir
18
+ from .paths import get_repo_root
21
19
 
22
20
 
23
21
  # =============================================================================
@@ -165,101 +163,13 @@ def archive_task_complete(
165
163
  return {}
166
164
 
167
165
 
168
- # =============================================================================
169
- # Task Directory Resolution
170
- # =============================================================================
171
-
172
- def resolve_task_dir(target_dir: str, repo_root: Path) -> Path:
173
- """Resolve task directory to absolute path.
174
-
175
- Supports:
176
- - Absolute path: /path/to/task
177
- - Relative path: .trellis/tasks/01-31-my-task
178
- - Task name: my-task (uses find_task_by_name for lookup)
179
-
180
- Args:
181
- target_dir: Task directory specification.
182
- repo_root: Repository root path.
183
-
184
- Returns:
185
- Resolved absolute path.
186
- """
187
- if not target_dir:
188
- return Path()
189
-
190
- # Absolute path
191
- if target_dir.startswith("/"):
192
- return Path(target_dir)
193
-
194
- # Relative path (contains path separator or starts with .trellis)
195
- if "/" in target_dir or target_dir.startswith(".trellis"):
196
- return repo_root / target_dir
197
-
198
- # Task name - try to find in tasks directory
199
- tasks_dir = get_tasks_dir(repo_root)
200
- found = find_task_by_name(target_dir, tasks_dir)
201
- if found:
202
- return found
203
-
204
- # Fallback to treating as relative path
205
- return repo_root / target_dir
206
-
207
-
208
- # =============================================================================
209
- # Lifecycle Hooks
210
- # =============================================================================
211
-
212
- def run_task_hooks(event: str, task_json_path: Path, repo_root: Path) -> None:
213
- """Run lifecycle hooks for a task event.
214
-
215
- Args:
216
- event: Event name (e.g. "after_create").
217
- task_json_path: Absolute path to the task's task.json.
218
- repo_root: Repository root for cwd and config lookup.
219
- """
220
- import os
221
- import subprocess
222
-
223
- from .config import get_hooks
224
- from .log import Colors, colored
225
-
226
- commands = get_hooks(event, repo_root)
227
- if not commands:
228
- return
229
-
230
- env = {**os.environ, "TASK_JSON_PATH": str(task_json_path)}
231
-
232
- for cmd in commands:
233
- try:
234
- result = subprocess.run(
235
- cmd,
236
- shell=True,
237
- cwd=repo_root,
238
- env=env,
239
- capture_output=True,
240
- text=True,
241
- encoding="utf-8",
242
- errors="replace",
243
- )
244
- if result.returncode != 0:
245
- print(
246
- colored(f"[WARN] Hook failed ({event}): {cmd}", Colors.YELLOW),
247
- file=sys.stderr,
248
- )
249
- if result.stderr.strip():
250
- print(f" {result.stderr.strip()}", file=sys.stderr)
251
- except Exception as e:
252
- print(
253
- colored(f"[WARN] Hook error ({event}): {cmd} — {e}", Colors.YELLOW),
254
- file=sys.stderr,
255
- )
256
-
257
-
258
166
  # =============================================================================
259
167
  # Main Entry (for testing)
260
168
  # =============================================================================
261
169
 
262
170
  if __name__ == "__main__":
171
+ from .paths import get_tasks_dir
172
+
263
173
  repo = get_repo_root()
264
174
  tasks = get_tasks_dir(repo)
265
175