@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.
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +203 -31
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +154 -6
- package/dist/commands/update.js.map +1 -1
- package/dist/configurators/workflow.d.ts +6 -2
- package/dist/configurators/workflow.d.ts.map +1 -1
- package/dist/configurators/workflow.js +88 -58
- package/dist/configurators/workflow.js.map +1 -1
- package/dist/migrations/index.d.ts +1 -0
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/migrations/index.js +2 -0
- package/dist/migrations/index.js.map +1 -1
- package/dist/migrations/manifests/0.3.9.json +9 -0
- package/dist/migrations/manifests/0.4.0-beta.1.json +228 -0
- package/dist/templates/claude/agents/dispatch.md +1 -2
- package/dist/templates/claude/agents/implement.md +2 -3
- package/dist/templates/claude/commands/trellis/before-dev.md +29 -0
- package/dist/templates/claude/commands/trellis/check.md +25 -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 +1 -2
- package/dist/templates/claude/commands/trellis/record-session.md +1 -1
- package/dist/templates/claude/commands/trellis/start.md +8 -4
- package/dist/templates/claude/hooks/inject-subagent-context.py +21 -13
- package/dist/templates/claude/hooks/session-start.py +170 -2
- package/dist/templates/codex/skills/before-dev/SKILL.md +34 -0
- package/dist/templates/codex/skills/check/SKILL.md +30 -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 +1 -1
- package/dist/templates/codex/skills/start/SKILL.md +8 -3
- package/dist/templates/cursor/commands/trellis-before-dev.md +29 -0
- package/dist/templates/cursor/commands/trellis-check.md +25 -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 +1 -1
- package/dist/templates/cursor/commands/trellis-start.md +7 -16
- package/dist/templates/gemini/commands/trellis/before-dev.toml +33 -0
- package/dist/templates/gemini/commands/trellis/check.toml +29 -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 +1 -1
- package/dist/templates/gemini/commands/trellis/start.toml +9 -4
- package/dist/templates/iflow/agents/dispatch.md +1 -2
- package/dist/templates/iflow/agents/implement.md +2 -3
- package/dist/templates/iflow/commands/trellis/before-dev.md +29 -0
- package/dist/templates/iflow/commands/trellis/check.md +25 -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 +1 -2
- package/dist/templates/iflow/commands/trellis/record-session.md +1 -1
- package/dist/templates/iflow/commands/trellis/start.md +8 -4
- package/dist/templates/iflow/hooks/inject-subagent-context.py +21 -13
- package/dist/templates/iflow/hooks/session-start.py +156 -1
- package/dist/templates/iflow/settings.json +2 -2
- package/dist/templates/kilo/workflows/before-dev.md +29 -0
- package/dist/templates/kilo/workflows/check.md +25 -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 +1 -2
- package/dist/templates/kilo/workflows/record-session.md +1 -1
- package/dist/templates/kilo/workflows/start.md +8 -3
- package/dist/templates/kiro/skills/before-dev/SKILL.md +34 -0
- package/dist/templates/kiro/skills/check/SKILL.md +30 -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 +1 -1
- package/dist/templates/kiro/skills/start/SKILL.md +8 -3
- package/dist/templates/markdown/spec/backend/script-conventions.md +93 -0
- package/dist/templates/opencode/agents/dispatch.md +1 -2
- package/dist/templates/opencode/agents/implement.md +2 -2
- package/dist/templates/opencode/agents/research.md +1 -2
- package/dist/templates/opencode/commands/trellis/before-dev.md +29 -0
- package/dist/templates/opencode/commands/trellis/check.md +25 -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 +1 -2
- package/dist/templates/opencode/commands/trellis/record-session.md +1 -1
- package/dist/templates/opencode/commands/trellis/start.md +8 -3
- package/dist/templates/opencode/plugin/inject-subagent-context.js +45 -18
- package/dist/templates/opencode/plugin/session-start.js +149 -1
- package/dist/templates/qoder/skills/before-dev/SKILL.md +34 -0
- package/dist/templates/qoder/skills/check/SKILL.md +30 -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 +1 -1
- package/dist/templates/qoder/skills/start/SKILL.md +8 -3
- package/dist/templates/trellis/config.yaml +20 -0
- package/dist/templates/trellis/index.d.ts +11 -0
- package/dist/templates/trellis/index.d.ts.map +1 -1
- package/dist/templates/trellis/index.js +22 -0
- package/dist/templates/trellis/index.js.map +1 -1
- package/dist/templates/trellis/scripts/add_session.py +52 -7
- package/dist/templates/trellis/scripts/common/cli_adapter.py +33 -45
- package/dist/templates/trellis/scripts/common/config.py +152 -0
- package/dist/templates/trellis/scripts/common/git.py +31 -0
- package/dist/templates/trellis/scripts/common/git_context.py +23 -586
- package/dist/templates/trellis/scripts/common/io.py +37 -0
- package/dist/templates/trellis/scripts/common/log.py +45 -0
- package/dist/templates/trellis/scripts/common/packages_context.py +233 -0
- package/dist/templates/trellis/scripts/common/paths.py +46 -0
- package/dist/templates/trellis/scripts/common/phase.py +50 -49
- package/dist/templates/trellis/scripts/common/registry.py +41 -72
- package/dist/templates/trellis/scripts/common/session_context.py +466 -0
- package/dist/templates/trellis/scripts/common/task_context.py +384 -0
- package/dist/templates/trellis/scripts/common/task_queue.py +27 -98
- package/dist/templates/trellis/scripts/common/task_store.py +534 -0
- package/dist/templates/trellis/scripts/common/task_utils.py +96 -6
- package/dist/templates/trellis/scripts/common/tasks.py +109 -0
- package/dist/templates/trellis/scripts/common/types.py +112 -0
- package/dist/templates/trellis/scripts/create_bootstrap.py +31 -26
- package/dist/templates/trellis/scripts/hooks/linear_sync.py +243 -0
- package/dist/templates/trellis/scripts/multi_agent/_bootstrap.py +17 -0
- package/dist/templates/trellis/scripts/multi_agent/cleanup.py +43 -48
- package/dist/templates/trellis/scripts/multi_agent/create_pr.py +336 -45
- package/dist/templates/trellis/scripts/multi_agent/plan.py +2 -26
- package/dist/templates/trellis/scripts/multi_agent/start.py +126 -57
- package/dist/templates/trellis/scripts/multi_agent/status.py +12 -753
- package/dist/templates/trellis/scripts/multi_agent/status_display.py +542 -0
- package/dist/templates/trellis/scripts/multi_agent/status_monitor.py +225 -0
- package/dist/templates/trellis/scripts/task.py +50 -975
- package/dist/templates/trellis/workflow.md +21 -34
- package/dist/types/migration.d.ts +3 -1
- package/dist/types/migration.d.ts.map +1 -1
- package/dist/utils/project-detector.d.ts +23 -0
- package/dist/utils/project-detector.d.ts.map +1 -1
- package/dist/utils/project-detector.js +364 -0
- package/dist/utils/project-detector.js.map +1 -1
- package/dist/utils/template-fetcher.d.ts +2 -2
- package/dist/utils/template-fetcher.d.ts.map +1 -1
- package/dist/utils/template-fetcher.js +5 -5
- package/dist/utils/template-fetcher.js.map +1 -1
- package/package.json +1 -1
- package/dist/templates/claude/commands/trellis/before-backend-dev.md +0 -13
- package/dist/templates/claude/commands/trellis/before-frontend-dev.md +0 -13
- package/dist/templates/claude/commands/trellis/check-backend.md +0 -13
- package/dist/templates/claude/commands/trellis/check-frontend.md +0 -13
- package/dist/templates/codex/skills/before-backend-dev/SKILL.md +0 -18
- package/dist/templates/codex/skills/before-frontend-dev/SKILL.md +0 -18
- package/dist/templates/codex/skills/check-backend/SKILL.md +0 -18
- package/dist/templates/codex/skills/check-frontend/SKILL.md +0 -18
- package/dist/templates/cursor/commands/trellis-before-backend-dev.md +0 -13
- package/dist/templates/cursor/commands/trellis-before-frontend-dev.md +0 -13
- package/dist/templates/cursor/commands/trellis-check-backend.md +0 -13
- package/dist/templates/cursor/commands/trellis-check-frontend.md +0 -13
- package/dist/templates/gemini/commands/trellis/before-backend-dev.toml +0 -17
- package/dist/templates/gemini/commands/trellis/before-frontend-dev.toml +0 -17
- package/dist/templates/gemini/commands/trellis/check-backend.toml +0 -17
- package/dist/templates/gemini/commands/trellis/check-frontend.toml +0 -17
- package/dist/templates/iflow/commands/trellis/before-backend-dev.md +0 -13
- package/dist/templates/iflow/commands/trellis/before-frontend-dev.md +0 -13
- package/dist/templates/iflow/commands/trellis/check-backend.md +0 -13
- package/dist/templates/iflow/commands/trellis/check-frontend.md +0 -13
- package/dist/templates/kilo/workflows/before-backend-dev.md +0 -13
- package/dist/templates/kilo/workflows/before-frontend-dev.md +0 -13
- package/dist/templates/kilo/workflows/check-backend.md +0 -13
- package/dist/templates/kilo/workflows/check-frontend.md +0 -13
- package/dist/templates/kiro/skills/before-backend-dev/SKILL.md +0 -18
- package/dist/templates/kiro/skills/before-frontend-dev/SKILL.md +0 -18
- package/dist/templates/kiro/skills/check-backend/SKILL.md +0 -18
- package/dist/templates/kiro/skills/check-frontend/SKILL.md +0 -18
- package/dist/templates/opencode/commands/trellis/before-backend-dev.md +0 -13
- package/dist/templates/opencode/commands/trellis/before-frontend-dev.md +0 -13
- package/dist/templates/opencode/commands/trellis/check-backend.md +0 -13
- package/dist/templates/opencode/commands/trellis/check-frontend.md +0 -13
- package/dist/templates/qoder/skills/before-backend-dev/SKILL.md +0 -18
- package/dist/templates/qoder/skills/before-frontend-dev/SKILL.md +0 -18
- package/dist/templates/qoder/skills/check-backend/SKILL.md +0 -18
- package/dist/templates/qoder/skills/check-frontend/SKILL.md +0 -18
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Task CRUD operations.
|
|
4
|
+
|
|
5
|
+
Provides:
|
|
6
|
+
ensure_tasks_dir - Ensure tasks directory exists
|
|
7
|
+
cmd_create - Create a new task
|
|
8
|
+
cmd_archive - Archive completed task
|
|
9
|
+
cmd_set_branch - Set git branch for task
|
|
10
|
+
cmd_set_base_branch - Set PR target branch
|
|
11
|
+
cmd_set_scope - Set scope for PR title
|
|
12
|
+
cmd_add_subtask - Link child task to parent
|
|
13
|
+
cmd_remove_subtask - Unlink child task from parent
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import re
|
|
20
|
+
import sys
|
|
21
|
+
from datetime import datetime
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
from .config import (
|
|
25
|
+
get_packages,
|
|
26
|
+
is_monorepo,
|
|
27
|
+
resolve_package,
|
|
28
|
+
validate_package,
|
|
29
|
+
)
|
|
30
|
+
from .git import run_git
|
|
31
|
+
from .io import read_json, write_json
|
|
32
|
+
from .log import Colors, colored
|
|
33
|
+
from .paths import (
|
|
34
|
+
DIR_ARCHIVE,
|
|
35
|
+
DIR_TASKS,
|
|
36
|
+
DIR_WORKFLOW,
|
|
37
|
+
FILE_TASK_JSON,
|
|
38
|
+
clear_current_task,
|
|
39
|
+
generate_task_date_prefix,
|
|
40
|
+
get_current_task,
|
|
41
|
+
get_developer,
|
|
42
|
+
get_repo_root,
|
|
43
|
+
get_tasks_dir,
|
|
44
|
+
)
|
|
45
|
+
from .task_utils import (
|
|
46
|
+
archive_task_complete,
|
|
47
|
+
find_task_by_name,
|
|
48
|
+
resolve_task_dir,
|
|
49
|
+
run_task_hooks,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# =============================================================================
|
|
54
|
+
# Helper Functions
|
|
55
|
+
# =============================================================================
|
|
56
|
+
|
|
57
|
+
def _slugify(title: str) -> str:
|
|
58
|
+
"""Convert title to slug (only works with ASCII)."""
|
|
59
|
+
result = title.lower()
|
|
60
|
+
result = re.sub(r"[^a-z0-9]", "-", result)
|
|
61
|
+
result = re.sub(r"-+", "-", result)
|
|
62
|
+
result = result.strip("-")
|
|
63
|
+
return result
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def ensure_tasks_dir(repo_root: Path) -> Path:
|
|
67
|
+
"""Ensure tasks directory exists."""
|
|
68
|
+
tasks_dir = get_tasks_dir(repo_root)
|
|
69
|
+
archive_dir = tasks_dir / "archive"
|
|
70
|
+
|
|
71
|
+
if not tasks_dir.exists():
|
|
72
|
+
tasks_dir.mkdir(parents=True)
|
|
73
|
+
print(colored(f"Created tasks directory: {tasks_dir}", Colors.GREEN), file=sys.stderr)
|
|
74
|
+
|
|
75
|
+
if not archive_dir.exists():
|
|
76
|
+
archive_dir.mkdir(parents=True)
|
|
77
|
+
|
|
78
|
+
return tasks_dir
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# =============================================================================
|
|
82
|
+
# Command: create
|
|
83
|
+
# =============================================================================
|
|
84
|
+
|
|
85
|
+
def cmd_create(args: argparse.Namespace) -> int:
|
|
86
|
+
"""Create a new task."""
|
|
87
|
+
repo_root = get_repo_root()
|
|
88
|
+
|
|
89
|
+
if not args.title:
|
|
90
|
+
print(colored("Error: title is required", Colors.RED), file=sys.stderr)
|
|
91
|
+
return 1
|
|
92
|
+
|
|
93
|
+
# Validate --package (CLI source: fail-fast)
|
|
94
|
+
package: str | None = getattr(args, "package", None)
|
|
95
|
+
if not is_monorepo(repo_root):
|
|
96
|
+
# Single-repo: ignore --package, no package prefix
|
|
97
|
+
if package:
|
|
98
|
+
print(colored(f"Warning: --package ignored in single-repo project", Colors.YELLOW), file=sys.stderr)
|
|
99
|
+
package = None
|
|
100
|
+
elif package:
|
|
101
|
+
if not validate_package(package, repo_root):
|
|
102
|
+
packages = get_packages(repo_root)
|
|
103
|
+
available = ", ".join(sorted(packages.keys())) if packages else "(none)"
|
|
104
|
+
print(colored(f"Error: unknown package '{package}'. Available: {available}", Colors.RED), file=sys.stderr)
|
|
105
|
+
return 1
|
|
106
|
+
else:
|
|
107
|
+
# Inferred: default_package → None (no task.json yet for create)
|
|
108
|
+
package = resolve_package(repo_root=repo_root)
|
|
109
|
+
|
|
110
|
+
# Default assignee to current developer
|
|
111
|
+
assignee = args.assignee
|
|
112
|
+
if not assignee:
|
|
113
|
+
assignee = get_developer(repo_root)
|
|
114
|
+
if not assignee:
|
|
115
|
+
print(colored("Error: No developer set. Run init_developer.py first or use --assignee", Colors.RED), file=sys.stderr)
|
|
116
|
+
return 1
|
|
117
|
+
|
|
118
|
+
ensure_tasks_dir(repo_root)
|
|
119
|
+
|
|
120
|
+
# Get current developer as creator
|
|
121
|
+
creator = get_developer(repo_root) or assignee
|
|
122
|
+
|
|
123
|
+
# Generate slug if not provided
|
|
124
|
+
slug = args.slug or _slugify(args.title)
|
|
125
|
+
if not slug:
|
|
126
|
+
print(colored("Error: could not generate slug from title", Colors.RED), file=sys.stderr)
|
|
127
|
+
return 1
|
|
128
|
+
|
|
129
|
+
# Create task directory with MM-DD-slug format
|
|
130
|
+
tasks_dir = get_tasks_dir(repo_root)
|
|
131
|
+
date_prefix = generate_task_date_prefix()
|
|
132
|
+
dir_name = f"{date_prefix}-{slug}"
|
|
133
|
+
task_dir = tasks_dir / dir_name
|
|
134
|
+
task_json_path = task_dir / FILE_TASK_JSON
|
|
135
|
+
|
|
136
|
+
if task_dir.exists():
|
|
137
|
+
print(colored(f"Warning: Task directory already exists: {dir_name}", Colors.YELLOW), file=sys.stderr)
|
|
138
|
+
else:
|
|
139
|
+
task_dir.mkdir(parents=True)
|
|
140
|
+
|
|
141
|
+
today = datetime.now().strftime("%Y-%m-%d")
|
|
142
|
+
|
|
143
|
+
# Record current branch as base_branch (PR target)
|
|
144
|
+
_, branch_out, _ = run_git(["branch", "--show-current"], cwd=repo_root)
|
|
145
|
+
current_branch = branch_out.strip() or "main"
|
|
146
|
+
|
|
147
|
+
task_data = {
|
|
148
|
+
"id": slug,
|
|
149
|
+
"name": slug,
|
|
150
|
+
"title": args.title,
|
|
151
|
+
"description": args.description or "",
|
|
152
|
+
"status": "planning",
|
|
153
|
+
"dev_type": None,
|
|
154
|
+
"scope": None,
|
|
155
|
+
"package": package,
|
|
156
|
+
"priority": args.priority,
|
|
157
|
+
"creator": creator,
|
|
158
|
+
"assignee": assignee,
|
|
159
|
+
"createdAt": today,
|
|
160
|
+
"completedAt": None,
|
|
161
|
+
"branch": None,
|
|
162
|
+
"base_branch": current_branch,
|
|
163
|
+
"worktree_path": None,
|
|
164
|
+
"current_phase": 0,
|
|
165
|
+
"next_action": [
|
|
166
|
+
{"phase": 1, "action": "implement"},
|
|
167
|
+
{"phase": 2, "action": "check"},
|
|
168
|
+
{"phase": 3, "action": "finish"},
|
|
169
|
+
{"phase": 4, "action": "create-pr"},
|
|
170
|
+
],
|
|
171
|
+
"commit": None,
|
|
172
|
+
"pr_url": None,
|
|
173
|
+
"subtasks": [],
|
|
174
|
+
"children": [],
|
|
175
|
+
"parent": None,
|
|
176
|
+
"relatedFiles": [],
|
|
177
|
+
"notes": "",
|
|
178
|
+
"meta": {},
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
write_json(task_json_path, task_data)
|
|
182
|
+
|
|
183
|
+
# Handle --parent: establish bidirectional link
|
|
184
|
+
if args.parent:
|
|
185
|
+
parent_dir = resolve_task_dir(args.parent, repo_root)
|
|
186
|
+
parent_json_path = parent_dir / FILE_TASK_JSON
|
|
187
|
+
if not parent_json_path.is_file():
|
|
188
|
+
print(colored(f"Warning: Parent task.json not found: {args.parent}", Colors.YELLOW), file=sys.stderr)
|
|
189
|
+
else:
|
|
190
|
+
parent_data = read_json(parent_json_path)
|
|
191
|
+
if parent_data:
|
|
192
|
+
# Add child to parent's children list
|
|
193
|
+
parent_children = parent_data.get("children", [])
|
|
194
|
+
if dir_name not in parent_children:
|
|
195
|
+
parent_children.append(dir_name)
|
|
196
|
+
parent_data["children"] = parent_children
|
|
197
|
+
write_json(parent_json_path, parent_data)
|
|
198
|
+
|
|
199
|
+
# Set parent in child's task.json
|
|
200
|
+
task_data["parent"] = parent_dir.name
|
|
201
|
+
write_json(task_json_path, task_data)
|
|
202
|
+
|
|
203
|
+
print(colored(f"Linked as child of: {parent_dir.name}", Colors.GREEN), file=sys.stderr)
|
|
204
|
+
|
|
205
|
+
print(colored(f"Created task: {dir_name}", Colors.GREEN), file=sys.stderr)
|
|
206
|
+
print("", file=sys.stderr)
|
|
207
|
+
print(colored("Next steps:", Colors.BLUE), file=sys.stderr)
|
|
208
|
+
print(" 1. Create prd.md with requirements", file=sys.stderr)
|
|
209
|
+
print(" 2. Run: python3 task.py init-context <dir> <dev_type>", file=sys.stderr)
|
|
210
|
+
print(" 3. Run: python3 task.py start <dir>", file=sys.stderr)
|
|
211
|
+
print("", file=sys.stderr)
|
|
212
|
+
|
|
213
|
+
# Output relative path for script chaining
|
|
214
|
+
print(f"{DIR_WORKFLOW}/{DIR_TASKS}/{dir_name}")
|
|
215
|
+
|
|
216
|
+
run_task_hooks("after_create", task_json_path, repo_root)
|
|
217
|
+
return 0
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
# =============================================================================
|
|
221
|
+
# Command: archive
|
|
222
|
+
# =============================================================================
|
|
223
|
+
|
|
224
|
+
def cmd_archive(args: argparse.Namespace) -> int:
|
|
225
|
+
"""Archive completed task."""
|
|
226
|
+
repo_root = get_repo_root()
|
|
227
|
+
task_name = args.name
|
|
228
|
+
|
|
229
|
+
if not task_name:
|
|
230
|
+
print(colored("Error: Task name is required", Colors.RED), file=sys.stderr)
|
|
231
|
+
return 1
|
|
232
|
+
|
|
233
|
+
tasks_dir = get_tasks_dir(repo_root)
|
|
234
|
+
|
|
235
|
+
# Find task directory
|
|
236
|
+
task_dir = find_task_by_name(task_name, tasks_dir)
|
|
237
|
+
|
|
238
|
+
if not task_dir or not task_dir.is_dir():
|
|
239
|
+
print(colored(f"Error: Task not found: {task_name}", Colors.RED), file=sys.stderr)
|
|
240
|
+
print("Active tasks:", file=sys.stderr)
|
|
241
|
+
# Import lazily to avoid circular dependency
|
|
242
|
+
from .tasks import iter_active_tasks
|
|
243
|
+
for t in iter_active_tasks(tasks_dir):
|
|
244
|
+
print(f" - {t.dir_name}/", file=sys.stderr)
|
|
245
|
+
return 1
|
|
246
|
+
|
|
247
|
+
dir_name = task_dir.name
|
|
248
|
+
task_json_path = task_dir / FILE_TASK_JSON
|
|
249
|
+
|
|
250
|
+
# Update status before archiving
|
|
251
|
+
today = datetime.now().strftime("%Y-%m-%d")
|
|
252
|
+
if task_json_path.is_file():
|
|
253
|
+
data = read_json(task_json_path)
|
|
254
|
+
if data:
|
|
255
|
+
data["status"] = "completed"
|
|
256
|
+
data["completedAt"] = today
|
|
257
|
+
write_json(task_json_path, data)
|
|
258
|
+
|
|
259
|
+
# Handle subtask relationships on archive
|
|
260
|
+
task_parent = data.get("parent")
|
|
261
|
+
task_children = data.get("children", [])
|
|
262
|
+
|
|
263
|
+
# If this is a child, remove from parent's children list
|
|
264
|
+
if task_parent:
|
|
265
|
+
parent_dir = find_task_by_name(task_parent, tasks_dir)
|
|
266
|
+
if parent_dir:
|
|
267
|
+
parent_json = parent_dir / FILE_TASK_JSON
|
|
268
|
+
if parent_json.is_file():
|
|
269
|
+
parent_data = read_json(parent_json)
|
|
270
|
+
if parent_data:
|
|
271
|
+
parent_children = parent_data.get("children", [])
|
|
272
|
+
if dir_name in parent_children:
|
|
273
|
+
parent_children.remove(dir_name)
|
|
274
|
+
parent_data["children"] = parent_children
|
|
275
|
+
write_json(parent_json, parent_data)
|
|
276
|
+
|
|
277
|
+
# If this is a parent, clear parent field in all children
|
|
278
|
+
if task_children:
|
|
279
|
+
for child_name in task_children:
|
|
280
|
+
child_dir_path = find_task_by_name(child_name, tasks_dir)
|
|
281
|
+
if child_dir_path:
|
|
282
|
+
child_json = child_dir_path / FILE_TASK_JSON
|
|
283
|
+
if child_json.is_file():
|
|
284
|
+
child_data = read_json(child_json)
|
|
285
|
+
if child_data:
|
|
286
|
+
child_data["parent"] = None
|
|
287
|
+
write_json(child_json, child_data)
|
|
288
|
+
|
|
289
|
+
# Clear if current task
|
|
290
|
+
current = get_current_task(repo_root)
|
|
291
|
+
if current and dir_name in current:
|
|
292
|
+
clear_current_task(repo_root)
|
|
293
|
+
|
|
294
|
+
# Archive
|
|
295
|
+
result = archive_task_complete(task_dir, repo_root)
|
|
296
|
+
if "archived_to" in result:
|
|
297
|
+
archive_dest = Path(result["archived_to"])
|
|
298
|
+
year_month = archive_dest.parent.name
|
|
299
|
+
print(colored(f"Archived: {dir_name} -> archive/{year_month}/", Colors.GREEN), file=sys.stderr)
|
|
300
|
+
|
|
301
|
+
# Auto-commit unless --no-commit
|
|
302
|
+
if not getattr(args, "no_commit", False):
|
|
303
|
+
_auto_commit_archive(dir_name, repo_root)
|
|
304
|
+
|
|
305
|
+
# Return the archive path
|
|
306
|
+
print(f"{DIR_WORKFLOW}/{DIR_TASKS}/{DIR_ARCHIVE}/{year_month}/{dir_name}")
|
|
307
|
+
|
|
308
|
+
# Run hooks with the archived path
|
|
309
|
+
archived_json = archive_dest / FILE_TASK_JSON
|
|
310
|
+
run_task_hooks("after_archive", archived_json, repo_root)
|
|
311
|
+
return 0
|
|
312
|
+
|
|
313
|
+
return 1
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _auto_commit_archive(task_name: str, repo_root: Path) -> None:
|
|
317
|
+
"""Stage .trellis/tasks/ changes and commit after archive."""
|
|
318
|
+
tasks_rel = f"{DIR_WORKFLOW}/{DIR_TASKS}"
|
|
319
|
+
run_git(["add", "-A", tasks_rel], cwd=repo_root)
|
|
320
|
+
|
|
321
|
+
# Check if there are staged changes
|
|
322
|
+
rc, _, _ = run_git(
|
|
323
|
+
["diff", "--cached", "--quiet", "--", tasks_rel], cwd=repo_root
|
|
324
|
+
)
|
|
325
|
+
if rc == 0:
|
|
326
|
+
print("[OK] No task changes to commit.", file=sys.stderr)
|
|
327
|
+
return
|
|
328
|
+
|
|
329
|
+
commit_msg = f"chore(task): archive {task_name}"
|
|
330
|
+
rc, _, err = run_git(["commit", "-m", commit_msg], cwd=repo_root)
|
|
331
|
+
if rc == 0:
|
|
332
|
+
print(f"[OK] Auto-committed: {commit_msg}", file=sys.stderr)
|
|
333
|
+
else:
|
|
334
|
+
print(f"[WARN] Auto-commit failed: {err.strip()}", file=sys.stderr)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
# =============================================================================
|
|
338
|
+
# Command: add-subtask
|
|
339
|
+
# =============================================================================
|
|
340
|
+
|
|
341
|
+
def cmd_add_subtask(args: argparse.Namespace) -> int:
|
|
342
|
+
"""Link a child task to a parent task."""
|
|
343
|
+
repo_root = get_repo_root()
|
|
344
|
+
|
|
345
|
+
parent_dir = resolve_task_dir(args.parent_dir, repo_root)
|
|
346
|
+
child_dir = resolve_task_dir(args.child_dir, repo_root)
|
|
347
|
+
|
|
348
|
+
parent_json_path = parent_dir / FILE_TASK_JSON
|
|
349
|
+
child_json_path = child_dir / FILE_TASK_JSON
|
|
350
|
+
|
|
351
|
+
if not parent_json_path.is_file():
|
|
352
|
+
print(colored(f"Error: Parent task.json not found: {args.parent_dir}", Colors.RED), file=sys.stderr)
|
|
353
|
+
return 1
|
|
354
|
+
|
|
355
|
+
if not child_json_path.is_file():
|
|
356
|
+
print(colored(f"Error: Child task.json not found: {args.child_dir}", Colors.RED), file=sys.stderr)
|
|
357
|
+
return 1
|
|
358
|
+
|
|
359
|
+
parent_data = read_json(parent_json_path)
|
|
360
|
+
child_data = read_json(child_json_path)
|
|
361
|
+
|
|
362
|
+
if not parent_data or not child_data:
|
|
363
|
+
print(colored("Error: Failed to read task.json", Colors.RED), file=sys.stderr)
|
|
364
|
+
return 1
|
|
365
|
+
|
|
366
|
+
# Check if child already has a parent
|
|
367
|
+
existing_parent = child_data.get("parent")
|
|
368
|
+
if existing_parent:
|
|
369
|
+
print(colored(f"Error: Child task already has a parent: {existing_parent}", Colors.RED), file=sys.stderr)
|
|
370
|
+
return 1
|
|
371
|
+
|
|
372
|
+
# Add child to parent's children list
|
|
373
|
+
parent_children = parent_data.get("children", [])
|
|
374
|
+
child_dir_name = child_dir.name
|
|
375
|
+
if child_dir_name not in parent_children:
|
|
376
|
+
parent_children.append(child_dir_name)
|
|
377
|
+
parent_data["children"] = parent_children
|
|
378
|
+
|
|
379
|
+
# Set parent in child's task.json
|
|
380
|
+
child_data["parent"] = parent_dir.name
|
|
381
|
+
|
|
382
|
+
# Write both
|
|
383
|
+
write_json(parent_json_path, parent_data)
|
|
384
|
+
write_json(child_json_path, child_data)
|
|
385
|
+
|
|
386
|
+
print(colored(f"Linked: {child_dir.name} -> {parent_dir.name}", Colors.GREEN), file=sys.stderr)
|
|
387
|
+
return 0
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
# =============================================================================
|
|
391
|
+
# Command: remove-subtask
|
|
392
|
+
# =============================================================================
|
|
393
|
+
|
|
394
|
+
def cmd_remove_subtask(args: argparse.Namespace) -> int:
|
|
395
|
+
"""Unlink a child task from a parent task."""
|
|
396
|
+
repo_root = get_repo_root()
|
|
397
|
+
|
|
398
|
+
parent_dir = resolve_task_dir(args.parent_dir, repo_root)
|
|
399
|
+
child_dir = resolve_task_dir(args.child_dir, repo_root)
|
|
400
|
+
|
|
401
|
+
parent_json_path = parent_dir / FILE_TASK_JSON
|
|
402
|
+
child_json_path = child_dir / FILE_TASK_JSON
|
|
403
|
+
|
|
404
|
+
if not parent_json_path.is_file():
|
|
405
|
+
print(colored(f"Error: Parent task.json not found: {args.parent_dir}", Colors.RED), file=sys.stderr)
|
|
406
|
+
return 1
|
|
407
|
+
|
|
408
|
+
if not child_json_path.is_file():
|
|
409
|
+
print(colored(f"Error: Child task.json not found: {args.child_dir}", Colors.RED), file=sys.stderr)
|
|
410
|
+
return 1
|
|
411
|
+
|
|
412
|
+
parent_data = read_json(parent_json_path)
|
|
413
|
+
child_data = read_json(child_json_path)
|
|
414
|
+
|
|
415
|
+
if not parent_data or not child_data:
|
|
416
|
+
print(colored("Error: Failed to read task.json", Colors.RED), file=sys.stderr)
|
|
417
|
+
return 1
|
|
418
|
+
|
|
419
|
+
# Remove child from parent's children list
|
|
420
|
+
parent_children = parent_data.get("children", [])
|
|
421
|
+
child_dir_name = child_dir.name
|
|
422
|
+
if child_dir_name in parent_children:
|
|
423
|
+
parent_children.remove(child_dir_name)
|
|
424
|
+
parent_data["children"] = parent_children
|
|
425
|
+
|
|
426
|
+
# Clear parent in child's task.json
|
|
427
|
+
child_data["parent"] = None
|
|
428
|
+
|
|
429
|
+
# Write both
|
|
430
|
+
write_json(parent_json_path, parent_data)
|
|
431
|
+
write_json(child_json_path, child_data)
|
|
432
|
+
|
|
433
|
+
print(colored(f"Unlinked: {child_dir.name} from {parent_dir.name}", Colors.GREEN), file=sys.stderr)
|
|
434
|
+
return 0
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
# =============================================================================
|
|
438
|
+
# Command: set-branch
|
|
439
|
+
# =============================================================================
|
|
440
|
+
|
|
441
|
+
def cmd_set_branch(args: argparse.Namespace) -> int:
|
|
442
|
+
"""Set git branch for task."""
|
|
443
|
+
repo_root = get_repo_root()
|
|
444
|
+
target_dir = resolve_task_dir(args.dir, repo_root)
|
|
445
|
+
branch = args.branch
|
|
446
|
+
|
|
447
|
+
if not branch:
|
|
448
|
+
print(colored("Error: Missing arguments", Colors.RED))
|
|
449
|
+
print("Usage: python3 task.py set-branch <task-dir> <branch-name>")
|
|
450
|
+
return 1
|
|
451
|
+
|
|
452
|
+
task_json = target_dir / FILE_TASK_JSON
|
|
453
|
+
if not task_json.is_file():
|
|
454
|
+
print(colored(f"Error: task.json not found at {target_dir}", Colors.RED))
|
|
455
|
+
return 1
|
|
456
|
+
|
|
457
|
+
data = read_json(task_json)
|
|
458
|
+
if not data:
|
|
459
|
+
return 1
|
|
460
|
+
|
|
461
|
+
data["branch"] = branch
|
|
462
|
+
write_json(task_json, data)
|
|
463
|
+
|
|
464
|
+
print(colored(f"✓ Branch set to: {branch}", Colors.GREEN))
|
|
465
|
+
print()
|
|
466
|
+
print(colored("Now you can start the multi-agent pipeline:", Colors.BLUE))
|
|
467
|
+
print(f" python3 ./.trellis/scripts/multi_agent/start.py {args.dir}")
|
|
468
|
+
return 0
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
# =============================================================================
|
|
472
|
+
# Command: set-base-branch
|
|
473
|
+
# =============================================================================
|
|
474
|
+
|
|
475
|
+
def cmd_set_base_branch(args: argparse.Namespace) -> int:
|
|
476
|
+
"""Set the base branch (PR target) for task."""
|
|
477
|
+
repo_root = get_repo_root()
|
|
478
|
+
target_dir = resolve_task_dir(args.dir, repo_root)
|
|
479
|
+
base_branch = args.base_branch
|
|
480
|
+
|
|
481
|
+
if not base_branch:
|
|
482
|
+
print(colored("Error: Missing arguments", Colors.RED))
|
|
483
|
+
print("Usage: python3 task.py set-base-branch <task-dir> <base-branch>")
|
|
484
|
+
print("Example: python3 task.py set-base-branch <dir> develop")
|
|
485
|
+
print()
|
|
486
|
+
print("This sets the target branch for PR (the branch your feature will merge into).")
|
|
487
|
+
return 1
|
|
488
|
+
|
|
489
|
+
task_json = target_dir / FILE_TASK_JSON
|
|
490
|
+
if not task_json.is_file():
|
|
491
|
+
print(colored(f"Error: task.json not found at {target_dir}", Colors.RED))
|
|
492
|
+
return 1
|
|
493
|
+
|
|
494
|
+
data = read_json(task_json)
|
|
495
|
+
if not data:
|
|
496
|
+
return 1
|
|
497
|
+
|
|
498
|
+
data["base_branch"] = base_branch
|
|
499
|
+
write_json(task_json, data)
|
|
500
|
+
|
|
501
|
+
print(colored(f"✓ Base branch set to: {base_branch}", Colors.GREEN))
|
|
502
|
+
print(f" PR will target: {base_branch}")
|
|
503
|
+
return 0
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
# =============================================================================
|
|
507
|
+
# Command: set-scope
|
|
508
|
+
# =============================================================================
|
|
509
|
+
|
|
510
|
+
def cmd_set_scope(args: argparse.Namespace) -> int:
|
|
511
|
+
"""Set scope for PR title."""
|
|
512
|
+
repo_root = get_repo_root()
|
|
513
|
+
target_dir = resolve_task_dir(args.dir, repo_root)
|
|
514
|
+
scope = args.scope
|
|
515
|
+
|
|
516
|
+
if not scope:
|
|
517
|
+
print(colored("Error: Missing arguments", Colors.RED))
|
|
518
|
+
print("Usage: python3 task.py set-scope <task-dir> <scope>")
|
|
519
|
+
return 1
|
|
520
|
+
|
|
521
|
+
task_json = target_dir / FILE_TASK_JSON
|
|
522
|
+
if not task_json.is_file():
|
|
523
|
+
print(colored(f"Error: task.json not found at {target_dir}", Colors.RED))
|
|
524
|
+
return 1
|
|
525
|
+
|
|
526
|
+
data = read_json(task_json)
|
|
527
|
+
if not data:
|
|
528
|
+
return 1
|
|
529
|
+
|
|
530
|
+
data["scope"] = scope
|
|
531
|
+
write_json(task_json, data)
|
|
532
|
+
|
|
533
|
+
print(colored(f"✓ Scope set to: {scope}", Colors.GREEN))
|
|
534
|
+
return 0
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
Task utility functions.
|
|
4
4
|
|
|
5
5
|
Provides:
|
|
6
|
-
is_safe_task_path
|
|
7
|
-
find_task_by_name
|
|
8
|
-
|
|
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
|
|
9
11
|
"""
|
|
10
12
|
|
|
11
13
|
from __future__ import annotations
|
|
@@ -15,7 +17,7 @@ import sys
|
|
|
15
17
|
from datetime import datetime
|
|
16
18
|
from pathlib import Path
|
|
17
19
|
|
|
18
|
-
from .paths import get_repo_root
|
|
20
|
+
from .paths import get_repo_root, get_tasks_dir
|
|
19
21
|
|
|
20
22
|
|
|
21
23
|
# =============================================================================
|
|
@@ -163,13 +165,101 @@ def archive_task_complete(
|
|
|
163
165
|
return {}
|
|
164
166
|
|
|
165
167
|
|
|
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
|
+
|
|
166
258
|
# =============================================================================
|
|
167
259
|
# Main Entry (for testing)
|
|
168
260
|
# =============================================================================
|
|
169
261
|
|
|
170
262
|
if __name__ == "__main__":
|
|
171
|
-
from .paths import get_tasks_dir
|
|
172
|
-
|
|
173
263
|
repo = get_repo_root()
|
|
174
264
|
tasks = get_tasks_dir(repo)
|
|
175
265
|
|