@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.
- package/dist/cli/index.js +0 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/init.d.ts +0 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +31 -203
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +6 -154
- package/dist/commands/update.js.map +1 -1
- package/dist/configurators/workflow.d.ts +2 -6
- package/dist/configurators/workflow.d.ts.map +1 -1
- package/dist/configurators/workflow.js +58 -88
- package/dist/configurators/workflow.js.map +1 -1
- package/dist/migrations/index.d.ts +0 -1
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/migrations/index.js +0 -2
- package/dist/migrations/index.js.map +1 -1
- package/dist/migrations/manifests/0.3.10.json +9 -0
- package/dist/templates/claude/agents/dispatch.md +2 -1
- package/dist/templates/claude/agents/implement.md +3 -2
- package/dist/templates/claude/commands/trellis/before-backend-dev.md +13 -0
- package/dist/templates/claude/commands/trellis/before-frontend-dev.md +13 -0
- package/dist/templates/claude/commands/trellis/check-backend.md +13 -0
- package/dist/templates/claude/commands/trellis/check-frontend.md +13 -0
- package/dist/templates/claude/commands/trellis/create-command.md +2 -2
- package/dist/templates/claude/commands/trellis/onboard.md +13 -13
- package/dist/templates/claude/commands/trellis/parallel.md +2 -1
- package/dist/templates/claude/commands/trellis/record-session.md +2 -2
- package/dist/templates/claude/commands/trellis/start.md +4 -8
- package/dist/templates/claude/hooks/inject-subagent-context.py +13 -21
- package/dist/templates/claude/hooks/session-start.py +2 -170
- package/dist/templates/codex/skills/before-backend-dev/SKILL.md +18 -0
- package/dist/templates/codex/skills/before-frontend-dev/SKILL.md +18 -0
- package/dist/templates/codex/skills/check-backend/SKILL.md +18 -0
- package/dist/templates/codex/skills/check-frontend/SKILL.md +18 -0
- package/dist/templates/codex/skills/create-command/SKILL.md +2 -2
- package/dist/templates/codex/skills/onboard/SKILL.md +11 -11
- package/dist/templates/codex/skills/record-session/SKILL.md +2 -2
- package/dist/templates/codex/skills/start/SKILL.md +3 -8
- package/dist/templates/cursor/commands/trellis-before-backend-dev.md +13 -0
- package/dist/templates/cursor/commands/trellis-before-frontend-dev.md +13 -0
- package/dist/templates/cursor/commands/trellis-check-backend.md +13 -0
- package/dist/templates/cursor/commands/trellis-check-frontend.md +13 -0
- package/dist/templates/cursor/commands/trellis-create-command.md +2 -2
- package/dist/templates/cursor/commands/trellis-onboard.md +13 -13
- package/dist/templates/cursor/commands/trellis-record-session.md +2 -2
- package/dist/templates/cursor/commands/trellis-start.md +16 -7
- package/dist/templates/gemini/commands/trellis/before-backend-dev.toml +17 -0
- package/dist/templates/gemini/commands/trellis/before-frontend-dev.toml +17 -0
- package/dist/templates/gemini/commands/trellis/check-backend.toml +17 -0
- package/dist/templates/gemini/commands/trellis/check-frontend.toml +17 -0
- package/dist/templates/gemini/commands/trellis/create-command.toml +2 -2
- package/dist/templates/gemini/commands/trellis/onboard.toml +2 -2
- package/dist/templates/gemini/commands/trellis/record-session.toml +2 -2
- package/dist/templates/gemini/commands/trellis/start.toml +4 -9
- package/dist/templates/iflow/agents/dispatch.md +2 -1
- package/dist/templates/iflow/agents/implement.md +3 -2
- package/dist/templates/iflow/commands/trellis/before-backend-dev.md +13 -0
- package/dist/templates/iflow/commands/trellis/before-frontend-dev.md +13 -0
- package/dist/templates/iflow/commands/trellis/check-backend.md +13 -0
- package/dist/templates/iflow/commands/trellis/check-frontend.md +13 -0
- package/dist/templates/iflow/commands/trellis/create-command.md +2 -2
- package/dist/templates/iflow/commands/trellis/onboard.md +13 -13
- package/dist/templates/iflow/commands/trellis/parallel.md +2 -1
- package/dist/templates/iflow/commands/trellis/record-session.md +2 -2
- package/dist/templates/iflow/commands/trellis/start.md +4 -8
- package/dist/templates/iflow/hooks/inject-subagent-context.py +13 -21
- package/dist/templates/iflow/hooks/session-start.py +1 -156
- package/dist/templates/kilo/workflows/before-backend-dev.md +13 -0
- package/dist/templates/kilo/workflows/before-frontend-dev.md +13 -0
- package/dist/templates/kilo/workflows/check-backend.md +13 -0
- package/dist/templates/kilo/workflows/check-frontend.md +13 -0
- package/dist/templates/kilo/workflows/create-command.md +2 -2
- package/dist/templates/kilo/workflows/onboard.md +13 -13
- package/dist/templates/kilo/workflows/parallel.md +2 -1
- package/dist/templates/kilo/workflows/record-session.md +2 -2
- package/dist/templates/kilo/workflows/start.md +3 -8
- package/dist/templates/kiro/skills/before-backend-dev/SKILL.md +18 -0
- package/dist/templates/kiro/skills/before-frontend-dev/SKILL.md +18 -0
- package/dist/templates/kiro/skills/check-backend/SKILL.md +18 -0
- package/dist/templates/kiro/skills/check-frontend/SKILL.md +18 -0
- package/dist/templates/kiro/skills/create-command/SKILL.md +2 -2
- package/dist/templates/kiro/skills/onboard/SKILL.md +11 -11
- package/dist/templates/kiro/skills/record-session/SKILL.md +2 -2
- package/dist/templates/kiro/skills/start/SKILL.md +3 -8
- package/dist/templates/markdown/spec/backend/script-conventions.md +0 -93
- package/dist/templates/opencode/agents/dispatch.md +2 -1
- package/dist/templates/opencode/agents/implement.md +2 -2
- package/dist/templates/opencode/agents/research.md +2 -1
- package/dist/templates/opencode/commands/trellis/before-backend-dev.md +13 -0
- package/dist/templates/opencode/commands/trellis/before-frontend-dev.md +13 -0
- package/dist/templates/opencode/commands/trellis/check-backend.md +13 -0
- package/dist/templates/opencode/commands/trellis/check-frontend.md +13 -0
- package/dist/templates/opencode/commands/trellis/create-command.md +2 -2
- package/dist/templates/opencode/commands/trellis/onboard.md +13 -13
- package/dist/templates/opencode/commands/trellis/parallel.md +2 -1
- package/dist/templates/opencode/commands/trellis/record-session.md +2 -2
- package/dist/templates/opencode/commands/trellis/start.md +3 -8
- package/dist/templates/opencode/plugin/inject-subagent-context.js +18 -45
- package/dist/templates/opencode/plugin/session-start.js +1 -149
- package/dist/templates/qoder/skills/before-backend-dev/SKILL.md +18 -0
- package/dist/templates/qoder/skills/before-frontend-dev/SKILL.md +18 -0
- package/dist/templates/qoder/skills/check-backend/SKILL.md +18 -0
- package/dist/templates/qoder/skills/check-frontend/SKILL.md +18 -0
- package/dist/templates/qoder/skills/create-command/SKILL.md +2 -2
- package/dist/templates/qoder/skills/onboard/SKILL.md +13 -13
- package/dist/templates/qoder/skills/record-session/SKILL.md +2 -2
- package/dist/templates/qoder/skills/start/SKILL.md +3 -8
- package/dist/templates/trellis/config.yaml +0 -20
- package/dist/templates/trellis/index.d.ts +0 -11
- package/dist/templates/trellis/index.d.ts.map +1 -1
- package/dist/templates/trellis/index.js +0 -22
- package/dist/templates/trellis/index.js.map +1 -1
- package/dist/templates/trellis/scripts/add_session.py +7 -52
- package/dist/templates/trellis/scripts/common/cli_adapter.py +45 -33
- package/dist/templates/trellis/scripts/common/config.py +0 -152
- package/dist/templates/trellis/scripts/common/git_context.py +586 -23
- package/dist/templates/trellis/scripts/common/paths.py +0 -46
- package/dist/templates/trellis/scripts/common/phase.py +49 -50
- package/dist/templates/trellis/scripts/common/registry.py +72 -41
- package/dist/templates/trellis/scripts/common/task_queue.py +98 -27
- package/dist/templates/trellis/scripts/common/task_utils.py +6 -96
- package/dist/templates/trellis/scripts/create_bootstrap.py +26 -31
- package/dist/templates/trellis/scripts/multi_agent/cleanup.py +48 -43
- package/dist/templates/trellis/scripts/multi_agent/create_pr.py +45 -336
- package/dist/templates/trellis/scripts/multi_agent/plan.py +26 -2
- package/dist/templates/trellis/scripts/multi_agent/start.py +57 -126
- package/dist/templates/trellis/scripts/multi_agent/status.py +753 -12
- package/dist/templates/trellis/scripts/task.py +975 -50
- package/dist/templates/trellis/workflow.md +34 -21
- package/dist/types/migration.d.ts +1 -3
- package/dist/types/migration.d.ts.map +1 -1
- package/dist/utils/project-detector.d.ts +0 -23
- package/dist/utils/project-detector.d.ts.map +1 -1
- package/dist/utils/project-detector.js +0 -364
- package/dist/utils/project-detector.js.map +1 -1
- package/dist/utils/template-fetcher.d.ts +10 -2
- package/dist/utils/template-fetcher.d.ts.map +1 -1
- package/dist/utils/template-fetcher.js +43 -12
- package/dist/utils/template-fetcher.js.map +1 -1
- package/package.json +1 -1
- package/dist/migrations/manifests/0.4.0-beta.1.json +0 -228
- package/dist/templates/claude/commands/trellis/before-dev.md +0 -29
- package/dist/templates/claude/commands/trellis/check.md +0 -25
- package/dist/templates/codex/skills/before-dev/SKILL.md +0 -34
- package/dist/templates/codex/skills/check/SKILL.md +0 -30
- package/dist/templates/cursor/commands/trellis-before-dev.md +0 -29
- package/dist/templates/cursor/commands/trellis-check.md +0 -25
- package/dist/templates/gemini/commands/trellis/before-dev.toml +0 -33
- package/dist/templates/gemini/commands/trellis/check.toml +0 -29
- package/dist/templates/iflow/commands/trellis/before-dev.md +0 -29
- package/dist/templates/iflow/commands/trellis/check.md +0 -25
- package/dist/templates/kilo/workflows/before-dev.md +0 -29
- package/dist/templates/kilo/workflows/check.md +0 -25
- package/dist/templates/kiro/skills/before-dev/SKILL.md +0 -34
- package/dist/templates/kiro/skills/check/SKILL.md +0 -30
- package/dist/templates/opencode/commands/trellis/before-dev.md +0 -29
- package/dist/templates/opencode/commands/trellis/check.md +0 -25
- package/dist/templates/qoder/skills/before-dev/SKILL.md +0 -34
- package/dist/templates/qoder/skills/check/SKILL.md +0 -30
- package/dist/templates/trellis/scripts/common/git.py +0 -31
- package/dist/templates/trellis/scripts/common/io.py +0 -37
- package/dist/templates/trellis/scripts/common/log.py +0 -45
- package/dist/templates/trellis/scripts/common/packages_context.py +0 -233
- package/dist/templates/trellis/scripts/common/session_context.py +0 -466
- package/dist/templates/trellis/scripts/common/task_context.py +0 -384
- package/dist/templates/trellis/scripts/common/task_store.py +0 -534
- package/dist/templates/trellis/scripts/common/tasks.py +0 -109
- package/dist/templates/trellis/scripts/common/types.py +0 -112
- package/dist/templates/trellis/scripts/hooks/linear_sync.py +0 -243
- package/dist/templates/trellis/scripts/multi_agent/_bootstrap.py +0 -17
- package/dist/templates/trellis/scripts/multi_agent/status_display.py +0 -542
- 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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
38
|
-
"""
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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 =
|
|
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 =
|
|
71
|
+
data = _read_json_file(task_json)
|
|
86
72
|
if not data:
|
|
87
73
|
return 0
|
|
88
|
-
|
|
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 =
|
|
91
|
+
data = _read_json_file(task_json)
|
|
102
92
|
if not data:
|
|
103
93
|
return "unknown"
|
|
104
|
-
|
|
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 =
|
|
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
|
-
|
|
122
|
-
action_name =
|
|
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/{
|
|
121
|
+
return f"0/{total_phases} (pending)"
|
|
126
122
|
else:
|
|
127
|
-
return f"{current_phase}/{
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
178
|
+
data = _read_json_file(task_json)
|
|
183
179
|
if not data:
|
|
184
180
|
return 0
|
|
185
|
-
|
|
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
|
-
|
|
233
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
196
|
-
if
|
|
197
|
-
return
|
|
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
|
-
|
|
221
|
-
|
|
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
|
|
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
|
-
|
|
244
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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
|
-
|
|
108
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
|
7
|
-
find_task_by_name
|
|
8
|
-
|
|
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
|
|
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
|
|