@mindfoldhq/trellis 0.3.4 → 0.3.6
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/README.md +6 -4
- package/dist/cli/index.js +1 -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 +243 -49
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +3 -0
- package/dist/commands/update.js.map +1 -1
- package/dist/migrations/manifests/0.3.4.json +1 -1
- package/dist/migrations/manifests/0.3.5.json +9 -0
- package/dist/migrations/manifests/0.3.6.json +9 -0
- package/dist/templates/claude/commands/trellis/brainstorm.md +13 -0
- package/dist/templates/claude/commands/trellis/record-session.md +4 -1
- package/dist/templates/claude/commands/trellis/start.md +4 -0
- package/dist/templates/claude/hooks/inject-subagent-context.py +1 -1
- package/dist/templates/claude/settings.json +10 -0
- package/dist/templates/codex/skills/brainstorm/SKILL.md +13 -0
- package/dist/templates/codex/skills/record-session/SKILL.md +4 -1
- package/dist/templates/codex/skills/start/SKILL.md +4 -0
- package/dist/templates/cursor/commands/trellis-brainstorm.md +13 -0
- package/dist/templates/cursor/commands/trellis-record-session.md +4 -1
- package/dist/templates/cursor/commands/trellis-start.md +4 -0
- package/dist/templates/gemini/commands/trellis/brainstorm.toml +15 -0
- package/dist/templates/gemini/commands/trellis/record-session.toml +4 -1
- package/dist/templates/gemini/commands/trellis/start.toml +4 -0
- package/dist/templates/iflow/commands/trellis/brainstorm.md +13 -0
- package/dist/templates/iflow/commands/trellis/record-session.md +4 -1
- package/dist/templates/iflow/commands/trellis/start.md +4 -0
- package/dist/templates/iflow/hooks/inject-subagent-context.py +1 -1
- package/dist/templates/kilo/workflows/brainstorm.md +13 -0
- package/dist/templates/kilo/workflows/record-session.md +4 -1
- package/dist/templates/kilo/workflows/start.md +4 -0
- package/dist/templates/kiro/skills/brainstorm/SKILL.md +13 -0
- package/dist/templates/kiro/skills/record-session/SKILL.md +4 -1
- package/dist/templates/kiro/skills/start/SKILL.md +4 -0
- package/dist/templates/markdown/spec/backend/directory-structure.md +292 -0
- package/dist/templates/markdown/spec/backend/script-conventions.md +220 -38
- package/dist/templates/opencode/commands/trellis/brainstorm.md +13 -0
- package/dist/templates/opencode/commands/trellis/record-session.md +4 -1
- package/dist/templates/opencode/commands/trellis/start.md +4 -0
- package/dist/templates/qoder/skills/brainstorm/SKILL.md +13 -0
- package/dist/templates/qoder/skills/record-session/SKILL.md +4 -1
- package/dist/templates/qoder/skills/start/SKILL.md +4 -0
- package/dist/templates/trellis/config.yaml +18 -0
- package/dist/templates/trellis/scripts/common/config.py +20 -0
- package/dist/templates/trellis/scripts/common/git_context.py +160 -12
- package/dist/templates/trellis/scripts/common/task_queue.py +4 -0
- package/dist/templates/trellis/scripts/common/worktree.py +78 -11
- package/dist/templates/trellis/scripts/create_bootstrap.py +3 -0
- package/dist/templates/trellis/scripts/task.py +312 -17
- package/dist/utils/template-fetcher.d.ts +57 -4
- package/dist/utils/template-fetcher.d.ts.map +1 -1
- package/dist/utils/template-fetcher.js +179 -10
- package/dist/utils/template-fetcher.js.map +1 -1
- package/package.json +7 -7
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
Task Management Script for Multi-Agent Pipeline.
|
|
5
5
|
|
|
6
6
|
Usage:
|
|
7
|
-
python3 task.py create "<title>" [--slug <name>] [--assignee <dev>] [--priority P0|P1|P2|P3]
|
|
7
|
+
python3 task.py create "<title>" [--slug <name>] [--assignee <dev>] [--priority P0|P1|P2|P3] [--parent <dir>]
|
|
8
8
|
python3 task.py init-context <dir> <type> # Initialize jsonl files
|
|
9
9
|
python3 task.py add-context <dir> <file> <path> [reason] # Add jsonl entry
|
|
10
10
|
python3 task.py validate <dir> # Validate jsonl files
|
|
@@ -18,6 +18,8 @@ Usage:
|
|
|
18
18
|
python3 task.py archive <task-name> # Archive completed task
|
|
19
19
|
python3 task.py list # List active tasks
|
|
20
20
|
python3 task.py list-archive [month] # List archived tasks
|
|
21
|
+
python3 task.py add-subtask <parent-dir> <child-dir> # Link child to parent
|
|
22
|
+
python3 task.py remove-subtask <parent-dir> <child-dir> # Unlink child from parent
|
|
21
23
|
"""
|
|
22
24
|
|
|
23
25
|
from __future__ import annotations
|
|
@@ -60,6 +62,7 @@ from common.task_utils import (
|
|
|
60
62
|
find_task_by_name,
|
|
61
63
|
archive_task_complete,
|
|
62
64
|
)
|
|
65
|
+
from common.config import get_hooks
|
|
63
66
|
|
|
64
67
|
|
|
65
68
|
# =============================================================================
|
|
@@ -80,6 +83,53 @@ def colored(text: str, color: str) -> str:
|
|
|
80
83
|
return f"{color}{text}{Colors.NC}"
|
|
81
84
|
|
|
82
85
|
|
|
86
|
+
# =============================================================================
|
|
87
|
+
# Lifecycle Hooks
|
|
88
|
+
# =============================================================================
|
|
89
|
+
|
|
90
|
+
def _run_hooks(event: str, task_json_path: Path, repo_root: Path) -> None:
|
|
91
|
+
"""Run lifecycle hooks for an event.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
event: Event name (e.g. "after_create").
|
|
95
|
+
task_json_path: Absolute path to the task's task.json.
|
|
96
|
+
repo_root: Repository root for cwd and config lookup.
|
|
97
|
+
"""
|
|
98
|
+
import os
|
|
99
|
+
import subprocess
|
|
100
|
+
|
|
101
|
+
commands = get_hooks(event, repo_root)
|
|
102
|
+
if not commands:
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
env = {**os.environ, "TASK_JSON_PATH": str(task_json_path)}
|
|
106
|
+
|
|
107
|
+
for cmd in commands:
|
|
108
|
+
try:
|
|
109
|
+
result = subprocess.run(
|
|
110
|
+
cmd,
|
|
111
|
+
shell=True,
|
|
112
|
+
cwd=repo_root,
|
|
113
|
+
env=env,
|
|
114
|
+
capture_output=True,
|
|
115
|
+
text=True,
|
|
116
|
+
encoding="utf-8",
|
|
117
|
+
errors="replace",
|
|
118
|
+
)
|
|
119
|
+
if result.returncode != 0:
|
|
120
|
+
print(
|
|
121
|
+
colored(f"[WARN] Hook failed ({event}): {cmd}", Colors.YELLOW),
|
|
122
|
+
file=sys.stderr,
|
|
123
|
+
)
|
|
124
|
+
if result.stderr.strip():
|
|
125
|
+
print(f" {result.stderr.strip()}", file=sys.stderr)
|
|
126
|
+
except Exception as e:
|
|
127
|
+
print(
|
|
128
|
+
colored(f"[WARN] Hook error ({event}): {cmd} — {e}", Colors.YELLOW),
|
|
129
|
+
file=sys.stderr,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
83
133
|
# =============================================================================
|
|
84
134
|
# Helper Functions
|
|
85
135
|
# =============================================================================
|
|
@@ -294,12 +344,37 @@ def cmd_create(args: argparse.Namespace) -> int:
|
|
|
294
344
|
"commit": None,
|
|
295
345
|
"pr_url": None,
|
|
296
346
|
"subtasks": [],
|
|
347
|
+
"children": [],
|
|
348
|
+
"parent": None,
|
|
297
349
|
"relatedFiles": [],
|
|
298
350
|
"notes": "",
|
|
351
|
+
"meta": {},
|
|
299
352
|
}
|
|
300
353
|
|
|
301
354
|
_write_json_file(task_json_path, task_data)
|
|
302
355
|
|
|
356
|
+
# Handle --parent: establish bidirectional link
|
|
357
|
+
if args.parent:
|
|
358
|
+
parent_dir = _resolve_task_dir(args.parent, repo_root)
|
|
359
|
+
parent_json_path = parent_dir / FILE_TASK_JSON
|
|
360
|
+
if not parent_json_path.is_file():
|
|
361
|
+
print(colored(f"Warning: Parent task.json not found: {args.parent}", Colors.YELLOW), file=sys.stderr)
|
|
362
|
+
else:
|
|
363
|
+
parent_data = _read_json_file(parent_json_path)
|
|
364
|
+
if parent_data:
|
|
365
|
+
# Add child to parent's children list
|
|
366
|
+
parent_children = parent_data.get("children", [])
|
|
367
|
+
if dir_name not in parent_children:
|
|
368
|
+
parent_children.append(dir_name)
|
|
369
|
+
parent_data["children"] = parent_children
|
|
370
|
+
_write_json_file(parent_json_path, parent_data)
|
|
371
|
+
|
|
372
|
+
# Set parent in child's task.json
|
|
373
|
+
task_data["parent"] = parent_dir.name
|
|
374
|
+
_write_json_file(task_json_path, task_data)
|
|
375
|
+
|
|
376
|
+
print(colored(f"Linked as child of: {parent_dir.name}", Colors.GREEN), file=sys.stderr)
|
|
377
|
+
|
|
303
378
|
print(colored(f"Created task: {dir_name}", Colors.GREEN), file=sys.stderr)
|
|
304
379
|
print("", file=sys.stderr)
|
|
305
380
|
print(colored("Next steps:", Colors.BLUE), file=sys.stderr)
|
|
@@ -310,6 +385,8 @@ def cmd_create(args: argparse.Namespace) -> int:
|
|
|
310
385
|
|
|
311
386
|
# Output relative path for script chaining
|
|
312
387
|
print(f"{DIR_WORKFLOW}/{DIR_TASKS}/{dir_name}")
|
|
388
|
+
|
|
389
|
+
_run_hooks("after_create", task_json_path, repo_root)
|
|
313
390
|
return 0
|
|
314
391
|
|
|
315
392
|
|
|
@@ -591,6 +668,9 @@ def cmd_start(args: argparse.Namespace) -> int:
|
|
|
591
668
|
print(colored(f"✓ Current task set to: {task_dir}", Colors.GREEN))
|
|
592
669
|
print()
|
|
593
670
|
print(colored("The hook will now inject context from this task's jsonl files.", Colors.BLUE))
|
|
671
|
+
|
|
672
|
+
task_json_path = full_path / FILE_TASK_JSON
|
|
673
|
+
_run_hooks("after_start", task_json_path, repo_root)
|
|
594
674
|
return 0
|
|
595
675
|
else:
|
|
596
676
|
print(colored("Error: Failed to set current task", Colors.RED))
|
|
@@ -606,8 +686,14 @@ def cmd_finish(args: argparse.Namespace) -> int:
|
|
|
606
686
|
print(colored("No current task set", Colors.YELLOW))
|
|
607
687
|
return 0
|
|
608
688
|
|
|
689
|
+
# Resolve task.json path before clearing
|
|
690
|
+
task_json_path = repo_root / current / FILE_TASK_JSON
|
|
691
|
+
|
|
609
692
|
clear_current_task(repo_root)
|
|
610
693
|
print(colored(f"✓ Cleared current task (was: {current})", Colors.GREEN))
|
|
694
|
+
|
|
695
|
+
if task_json_path.is_file():
|
|
696
|
+
_run_hooks("after_finish", task_json_path, repo_root)
|
|
611
697
|
return 0
|
|
612
698
|
|
|
613
699
|
|
|
@@ -647,6 +733,36 @@ def cmd_archive(args: argparse.Namespace) -> int:
|
|
|
647
733
|
data["completedAt"] = today
|
|
648
734
|
_write_json_file(task_json_path, data)
|
|
649
735
|
|
|
736
|
+
# Handle subtask relationships on archive
|
|
737
|
+
task_parent = data.get("parent")
|
|
738
|
+
task_children = data.get("children", [])
|
|
739
|
+
|
|
740
|
+
# If this is a child, remove from parent's children list
|
|
741
|
+
if task_parent:
|
|
742
|
+
parent_dir = find_task_by_name(task_parent, tasks_dir)
|
|
743
|
+
if parent_dir:
|
|
744
|
+
parent_json = parent_dir / FILE_TASK_JSON
|
|
745
|
+
if parent_json.is_file():
|
|
746
|
+
parent_data = _read_json_file(parent_json)
|
|
747
|
+
if parent_data:
|
|
748
|
+
parent_children = parent_data.get("children", [])
|
|
749
|
+
if dir_name in parent_children:
|
|
750
|
+
parent_children.remove(dir_name)
|
|
751
|
+
parent_data["children"] = parent_children
|
|
752
|
+
_write_json_file(parent_json, parent_data)
|
|
753
|
+
|
|
754
|
+
# If this is a parent, clear parent field in all children
|
|
755
|
+
if task_children:
|
|
756
|
+
for child_name in task_children:
|
|
757
|
+
child_dir_path = find_task_by_name(child_name, tasks_dir)
|
|
758
|
+
if child_dir_path:
|
|
759
|
+
child_json = child_dir_path / FILE_TASK_JSON
|
|
760
|
+
if child_json.is_file():
|
|
761
|
+
child_data = _read_json_file(child_json)
|
|
762
|
+
if child_data:
|
|
763
|
+
child_data["parent"] = None
|
|
764
|
+
_write_json_file(child_json, child_data)
|
|
765
|
+
|
|
650
766
|
# Clear if current task
|
|
651
767
|
current = get_current_task(repo_root)
|
|
652
768
|
if current and dir_name in current:
|
|
@@ -665,6 +781,10 @@ def cmd_archive(args: argparse.Namespace) -> int:
|
|
|
665
781
|
|
|
666
782
|
# Return the archive path
|
|
667
783
|
print(f"{DIR_WORKFLOW}/{DIR_TASKS}/{DIR_ARCHIVE}/{year_month}/{dir_name}")
|
|
784
|
+
|
|
785
|
+
# Run hooks with the archived path
|
|
786
|
+
archived_json = archive_dest / FILE_TASK_JSON
|
|
787
|
+
_run_hooks("after_archive", archived_json, repo_root)
|
|
668
788
|
return 0
|
|
669
789
|
|
|
670
790
|
return 1
|
|
@@ -691,10 +811,128 @@ def _auto_commit_archive(task_name: str, repo_root: Path) -> None:
|
|
|
691
811
|
print(f"[WARN] Auto-commit failed: {err.strip()}", file=sys.stderr)
|
|
692
812
|
|
|
693
813
|
|
|
814
|
+
# =============================================================================
|
|
815
|
+
# Command: add-subtask
|
|
816
|
+
# =============================================================================
|
|
817
|
+
|
|
818
|
+
def cmd_add_subtask(args: argparse.Namespace) -> int:
|
|
819
|
+
"""Link a child task to a parent task."""
|
|
820
|
+
repo_root = get_repo_root()
|
|
821
|
+
|
|
822
|
+
parent_dir = _resolve_task_dir(args.parent_dir, repo_root)
|
|
823
|
+
child_dir = _resolve_task_dir(args.child_dir, repo_root)
|
|
824
|
+
|
|
825
|
+
parent_json_path = parent_dir / FILE_TASK_JSON
|
|
826
|
+
child_json_path = child_dir / FILE_TASK_JSON
|
|
827
|
+
|
|
828
|
+
if not parent_json_path.is_file():
|
|
829
|
+
print(colored(f"Error: Parent task.json not found: {args.parent_dir}", Colors.RED), file=sys.stderr)
|
|
830
|
+
return 1
|
|
831
|
+
|
|
832
|
+
if not child_json_path.is_file():
|
|
833
|
+
print(colored(f"Error: Child task.json not found: {args.child_dir}", Colors.RED), file=sys.stderr)
|
|
834
|
+
return 1
|
|
835
|
+
|
|
836
|
+
parent_data = _read_json_file(parent_json_path)
|
|
837
|
+
child_data = _read_json_file(child_json_path)
|
|
838
|
+
|
|
839
|
+
if not parent_data or not child_data:
|
|
840
|
+
print(colored("Error: Failed to read task.json", Colors.RED), file=sys.stderr)
|
|
841
|
+
return 1
|
|
842
|
+
|
|
843
|
+
# Check if child already has a parent
|
|
844
|
+
existing_parent = child_data.get("parent")
|
|
845
|
+
if existing_parent:
|
|
846
|
+
print(colored(f"Error: Child task already has a parent: {existing_parent}", Colors.RED), file=sys.stderr)
|
|
847
|
+
return 1
|
|
848
|
+
|
|
849
|
+
# Add child to parent's children list
|
|
850
|
+
parent_children = parent_data.get("children", [])
|
|
851
|
+
child_dir_name = child_dir.name
|
|
852
|
+
if child_dir_name not in parent_children:
|
|
853
|
+
parent_children.append(child_dir_name)
|
|
854
|
+
parent_data["children"] = parent_children
|
|
855
|
+
|
|
856
|
+
# Set parent in child's task.json
|
|
857
|
+
child_data["parent"] = parent_dir.name
|
|
858
|
+
|
|
859
|
+
# Write both
|
|
860
|
+
_write_json_file(parent_json_path, parent_data)
|
|
861
|
+
_write_json_file(child_json_path, child_data)
|
|
862
|
+
|
|
863
|
+
print(colored(f"Linked: {child_dir.name} -> {parent_dir.name}", Colors.GREEN), file=sys.stderr)
|
|
864
|
+
return 0
|
|
865
|
+
|
|
866
|
+
|
|
867
|
+
# =============================================================================
|
|
868
|
+
# Command: remove-subtask
|
|
869
|
+
# =============================================================================
|
|
870
|
+
|
|
871
|
+
def cmd_remove_subtask(args: argparse.Namespace) -> int:
|
|
872
|
+
"""Unlink a child task from a parent task."""
|
|
873
|
+
repo_root = get_repo_root()
|
|
874
|
+
|
|
875
|
+
parent_dir = _resolve_task_dir(args.parent_dir, repo_root)
|
|
876
|
+
child_dir = _resolve_task_dir(args.child_dir, repo_root)
|
|
877
|
+
|
|
878
|
+
parent_json_path = parent_dir / FILE_TASK_JSON
|
|
879
|
+
child_json_path = child_dir / FILE_TASK_JSON
|
|
880
|
+
|
|
881
|
+
if not parent_json_path.is_file():
|
|
882
|
+
print(colored(f"Error: Parent task.json not found: {args.parent_dir}", Colors.RED), file=sys.stderr)
|
|
883
|
+
return 1
|
|
884
|
+
|
|
885
|
+
if not child_json_path.is_file():
|
|
886
|
+
print(colored(f"Error: Child task.json not found: {args.child_dir}", Colors.RED), file=sys.stderr)
|
|
887
|
+
return 1
|
|
888
|
+
|
|
889
|
+
parent_data = _read_json_file(parent_json_path)
|
|
890
|
+
child_data = _read_json_file(child_json_path)
|
|
891
|
+
|
|
892
|
+
if not parent_data or not child_data:
|
|
893
|
+
print(colored("Error: Failed to read task.json", Colors.RED), file=sys.stderr)
|
|
894
|
+
return 1
|
|
895
|
+
|
|
896
|
+
# Remove child from parent's children list
|
|
897
|
+
parent_children = parent_data.get("children", [])
|
|
898
|
+
child_dir_name = child_dir.name
|
|
899
|
+
if child_dir_name in parent_children:
|
|
900
|
+
parent_children.remove(child_dir_name)
|
|
901
|
+
parent_data["children"] = parent_children
|
|
902
|
+
|
|
903
|
+
# Clear parent in child's task.json
|
|
904
|
+
child_data["parent"] = None
|
|
905
|
+
|
|
906
|
+
# Write both
|
|
907
|
+
_write_json_file(parent_json_path, parent_data)
|
|
908
|
+
_write_json_file(child_json_path, child_data)
|
|
909
|
+
|
|
910
|
+
print(colored(f"Unlinked: {child_dir.name} from {parent_dir.name}", Colors.GREEN), file=sys.stderr)
|
|
911
|
+
return 0
|
|
912
|
+
|
|
913
|
+
|
|
694
914
|
# =============================================================================
|
|
695
915
|
# Command: list
|
|
696
916
|
# =============================================================================
|
|
697
917
|
|
|
918
|
+
def _get_children_progress(children: list[str], tasks_dir: Path) -> str:
|
|
919
|
+
"""Get children progress summary like '[2/3 done]'."""
|
|
920
|
+
if not children:
|
|
921
|
+
return ""
|
|
922
|
+
done_count = 0
|
|
923
|
+
total = len(children)
|
|
924
|
+
for child_name in children:
|
|
925
|
+
child_dir = tasks_dir / child_name
|
|
926
|
+
child_json = child_dir / FILE_TASK_JSON
|
|
927
|
+
if child_json.is_file():
|
|
928
|
+
data = _read_json_file(child_json)
|
|
929
|
+
if data:
|
|
930
|
+
status = data.get("status", "")
|
|
931
|
+
if status in ("completed", "done"):
|
|
932
|
+
done_count += 1
|
|
933
|
+
return f" [{done_count}/{total} done]"
|
|
934
|
+
|
|
935
|
+
|
|
698
936
|
def cmd_list(args: argparse.Namespace) -> int:
|
|
699
937
|
"""List active tasks."""
|
|
700
938
|
repo_root = get_repo_root()
|
|
@@ -713,7 +951,8 @@ def cmd_list(args: argparse.Namespace) -> int:
|
|
|
713
951
|
print(colored("All active tasks:", Colors.BLUE))
|
|
714
952
|
print()
|
|
715
953
|
|
|
716
|
-
|
|
954
|
+
# First pass: collect all task data and identify parent/child relationships
|
|
955
|
+
all_tasks: dict[str, dict] = {}
|
|
717
956
|
if tasks_dir.is_dir():
|
|
718
957
|
for d in sorted(tasks_dir.iterdir()):
|
|
719
958
|
if not d.is_dir() or d.name == "archive":
|
|
@@ -723,31 +962,68 @@ def cmd_list(args: argparse.Namespace) -> int:
|
|
|
723
962
|
task_json = d / FILE_TASK_JSON
|
|
724
963
|
status = "unknown"
|
|
725
964
|
assignee = "-"
|
|
726
|
-
|
|
965
|
+
children: list[str] = []
|
|
966
|
+
parent: str | None = None
|
|
727
967
|
|
|
728
968
|
if task_json.is_file():
|
|
729
969
|
data = _read_json_file(task_json)
|
|
730
970
|
if data:
|
|
731
971
|
status = data.get("status", "unknown")
|
|
732
972
|
assignee = data.get("assignee", "-")
|
|
973
|
+
children = data.get("children", [])
|
|
974
|
+
parent = data.get("parent")
|
|
733
975
|
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
976
|
+
all_tasks[dir_name] = {
|
|
977
|
+
"status": status,
|
|
978
|
+
"assignee": assignee,
|
|
979
|
+
"children": children,
|
|
980
|
+
"parent": parent,
|
|
981
|
+
}
|
|
737
982
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
continue
|
|
983
|
+
# Second pass: display tasks hierarchically
|
|
984
|
+
count = 0
|
|
741
985
|
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
986
|
+
def _print_task(dir_name: str, indent: int = 0) -> None:
|
|
987
|
+
nonlocal count
|
|
988
|
+
info = all_tasks[dir_name]
|
|
989
|
+
status = info["status"]
|
|
990
|
+
assignee = info["assignee"]
|
|
991
|
+
children = info["children"]
|
|
745
992
|
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
993
|
+
# Apply --mine filter
|
|
994
|
+
if filter_mine and assignee != developer:
|
|
995
|
+
return
|
|
996
|
+
|
|
997
|
+
# Apply --status filter
|
|
998
|
+
if filter_status and status != filter_status:
|
|
999
|
+
return
|
|
1000
|
+
|
|
1001
|
+
relative_path = f"{DIR_WORKFLOW}/{DIR_TASKS}/{dir_name}"
|
|
1002
|
+
marker = ""
|
|
1003
|
+
if relative_path == current_task:
|
|
1004
|
+
marker = f" {colored('<- current', Colors.GREEN)}"
|
|
1005
|
+
|
|
1006
|
+
# Children progress
|
|
1007
|
+
progress = _get_children_progress(children, tasks_dir) if children else ""
|
|
1008
|
+
|
|
1009
|
+
prefix = " " * indent + " - "
|
|
1010
|
+
|
|
1011
|
+
if filter_mine:
|
|
1012
|
+
print(f"{prefix}{dir_name}/ ({status}){progress}{marker}")
|
|
1013
|
+
else:
|
|
1014
|
+
print(f"{prefix}{dir_name}/ ({status}){progress} [{colored(assignee, Colors.CYAN)}]{marker}")
|
|
1015
|
+
count += 1
|
|
1016
|
+
|
|
1017
|
+
# Print children indented
|
|
1018
|
+
for child_name in children:
|
|
1019
|
+
if child_name in all_tasks:
|
|
1020
|
+
_print_task(child_name, indent + 1)
|
|
1021
|
+
|
|
1022
|
+
# Display only top-level tasks (those without a parent)
|
|
1023
|
+
for dir_name in sorted(all_tasks.keys()):
|
|
1024
|
+
info = all_tasks[dir_name]
|
|
1025
|
+
if not info["parent"]:
|
|
1026
|
+
_print_task(dir_name)
|
|
751
1027
|
|
|
752
1028
|
if count == 0:
|
|
753
1029
|
if filter_mine:
|
|
@@ -924,6 +1200,7 @@ def show_usage() -> None:
|
|
|
924
1200
|
|
|
925
1201
|
Usage:
|
|
926
1202
|
python3 task.py create <title> Create new task directory
|
|
1203
|
+
python3 task.py create <title> --parent <dir> Create task as child of parent
|
|
927
1204
|
python3 task.py init-context <dir> <dev_type> Initialize jsonl files
|
|
928
1205
|
python3 task.py add-context <dir> <jsonl> <path> [reason] Add entry to jsonl
|
|
929
1206
|
python3 task.py validate <dir> Validate jsonl files
|
|
@@ -934,6 +1211,8 @@ Usage:
|
|
|
934
1211
|
python3 task.py set-scope <dir> <scope> Set scope for PR title
|
|
935
1212
|
python3 task.py create-pr [dir] [--dry-run] Create PR from task
|
|
936
1213
|
python3 task.py archive <task-name> Archive completed task
|
|
1214
|
+
python3 task.py add-subtask <parent> <child> Link child task to parent
|
|
1215
|
+
python3 task.py remove-subtask <parent> <child> Unlink child from parent
|
|
937
1216
|
python3 task.py list [--mine] [--status <status>] List tasks
|
|
938
1217
|
python3 task.py list-archive [YYYY-MM] List archived tasks
|
|
939
1218
|
|
|
@@ -946,6 +1225,7 @@ List options:
|
|
|
946
1225
|
|
|
947
1226
|
Examples:
|
|
948
1227
|
python3 task.py create "Add login feature" --slug add-login
|
|
1228
|
+
python3 task.py create "Child task" --slug child --parent .trellis/tasks/01-21-parent
|
|
949
1229
|
python3 task.py init-context .trellis/tasks/01-21-add-login backend
|
|
950
1230
|
python3 task.py add-context <dir> implement .trellis/spec/backend/auth.md "Auth guidelines"
|
|
951
1231
|
python3 task.py set-branch <dir> task/add-login
|
|
@@ -954,6 +1234,8 @@ Examples:
|
|
|
954
1234
|
python3 task.py create-pr <dir> --dry-run # Preview without changes
|
|
955
1235
|
python3 task.py finish
|
|
956
1236
|
python3 task.py archive add-login
|
|
1237
|
+
python3 task.py add-subtask parent-task child-task # Link existing tasks
|
|
1238
|
+
python3 task.py remove-subtask parent-task child-task
|
|
957
1239
|
python3 task.py list # List all active tasks
|
|
958
1240
|
python3 task.py list --mine # List my tasks only
|
|
959
1241
|
python3 task.py list --mine --status in_progress # List my in-progress tasks
|
|
@@ -979,6 +1261,7 @@ def main() -> int:
|
|
|
979
1261
|
p_create.add_argument("--assignee", "-a", help="Assignee developer")
|
|
980
1262
|
p_create.add_argument("--priority", "-p", default="P2", help="Priority (P0-P3)")
|
|
981
1263
|
p_create.add_argument("--description", "-d", help="Task description")
|
|
1264
|
+
p_create.add_argument("--parent", help="Parent task directory (establishes subtask link)")
|
|
982
1265
|
|
|
983
1266
|
# init-context
|
|
984
1267
|
p_init = subparsers.add_parser("init-context", help="Initialize context files")
|
|
@@ -1037,6 +1320,16 @@ def main() -> int:
|
|
|
1037
1320
|
p_list.add_argument("--mine", "-m", action="store_true", help="My tasks only")
|
|
1038
1321
|
p_list.add_argument("--status", "-s", help="Filter by status")
|
|
1039
1322
|
|
|
1323
|
+
# add-subtask
|
|
1324
|
+
p_addsub = subparsers.add_parser("add-subtask", help="Link child task to parent")
|
|
1325
|
+
p_addsub.add_argument("parent_dir", help="Parent task directory")
|
|
1326
|
+
p_addsub.add_argument("child_dir", help="Child task directory")
|
|
1327
|
+
|
|
1328
|
+
# remove-subtask
|
|
1329
|
+
p_rmsub = subparsers.add_parser("remove-subtask", help="Unlink child task from parent")
|
|
1330
|
+
p_rmsub.add_argument("parent_dir", help="Parent task directory")
|
|
1331
|
+
p_rmsub.add_argument("child_dir", help="Child task directory")
|
|
1332
|
+
|
|
1040
1333
|
# list-archive
|
|
1041
1334
|
p_listarch = subparsers.add_parser("list-archive", help="List archived tasks")
|
|
1042
1335
|
p_listarch.add_argument("month", nargs="?", help="Month (YYYY-MM)")
|
|
@@ -1060,6 +1353,8 @@ def main() -> int:
|
|
|
1060
1353
|
"set-scope": cmd_set_scope,
|
|
1061
1354
|
"create-pr": cmd_create_pr,
|
|
1062
1355
|
"archive": cmd_archive,
|
|
1356
|
+
"add-subtask": cmd_add_subtask,
|
|
1357
|
+
"remove-subtask": cmd_remove_subtask,
|
|
1063
1358
|
"list": cmd_list,
|
|
1064
1359
|
"list-archive": cmd_list_archive,
|
|
1065
1360
|
}
|
|
@@ -21,15 +21,53 @@ export interface SpecTemplate {
|
|
|
21
21
|
tags?: string[];
|
|
22
22
|
}
|
|
23
23
|
export type TemplateStrategy = "skip" | "overwrite" | "append";
|
|
24
|
+
export interface RegistrySource {
|
|
25
|
+
/** Original provider prefix (e.g., "gh", "gitlab", "bitbucket") */
|
|
26
|
+
provider: string;
|
|
27
|
+
/** Repository path (e.g., "myorg/myrepo") */
|
|
28
|
+
repo: string;
|
|
29
|
+
/** Subdirectory within the repo (e.g., "marketplace" or "specs/my-template") */
|
|
30
|
+
subdir: string;
|
|
31
|
+
/** Git ref / branch (default: "main") */
|
|
32
|
+
ref: string;
|
|
33
|
+
/** Base URL for fetching raw files (e.g., index.json) */
|
|
34
|
+
rawBaseUrl: string;
|
|
35
|
+
/** Full giget source string for downloading */
|
|
36
|
+
gigetSource: string;
|
|
37
|
+
}
|
|
38
|
+
export declare const SUPPORTED_PROVIDERS: string[];
|
|
39
|
+
/**
|
|
40
|
+
* Parse a giget-style registry source into its components.
|
|
41
|
+
*
|
|
42
|
+
* Supports: gh:user/repo/subdir#ref, gitlab:user/repo/subdir, bitbucket:user/repo/subdir
|
|
43
|
+
* Ref defaults to "main" if not specified.
|
|
44
|
+
*
|
|
45
|
+
* @throws Error if provider is unsupported
|
|
46
|
+
*/
|
|
47
|
+
export declare function parseRegistrySource(source: string): RegistrySource;
|
|
24
48
|
/**
|
|
25
49
|
* Fetch available templates from the remote index
|
|
26
50
|
* Returns empty array on network error or timeout (allows fallback to blank)
|
|
51
|
+
*
|
|
52
|
+
* @param indexUrl - URL to fetch index.json from (defaults to official marketplace)
|
|
53
|
+
*/
|
|
54
|
+
export declare function fetchTemplateIndex(indexUrl?: string): Promise<SpecTemplate[]>;
|
|
55
|
+
/**
|
|
56
|
+
* Probe a registry's index.json, distinguishing "not found" from transient errors.
|
|
57
|
+
* Used by the registry flow to decide marketplace vs direct-download mode.
|
|
58
|
+
*
|
|
59
|
+
* - 404 → { templates: [], isNotFound: true }
|
|
60
|
+
* - Other HTTP error / network timeout → { templates: [], isNotFound: false }
|
|
61
|
+
* - 200 + valid JSON → { templates: [...], isNotFound: false }
|
|
27
62
|
*/
|
|
28
|
-
export declare function
|
|
63
|
+
export declare function probeRegistryIndex(indexUrl: string): Promise<{
|
|
64
|
+
templates: SpecTemplate[];
|
|
65
|
+
isNotFound: boolean;
|
|
66
|
+
}>;
|
|
29
67
|
/**
|
|
30
68
|
* Find a template by ID from the index
|
|
31
69
|
*/
|
|
32
|
-
export declare function findTemplate(templateId: string): Promise<SpecTemplate | null>;
|
|
70
|
+
export declare function findTemplate(templateId: string, indexUrl?: string): Promise<SpecTemplate | null>;
|
|
33
71
|
/**
|
|
34
72
|
* Get the installation path for a template type
|
|
35
73
|
*/
|
|
@@ -38,11 +76,15 @@ export declare function getInstallPath(cwd: string, templateType: string): strin
|
|
|
38
76
|
* Download a template with the specified strategy
|
|
39
77
|
*
|
|
40
78
|
* @param templatePath - Path in the docs repo (e.g., "marketplace/specs/electron-fullstack")
|
|
79
|
+
* OR a full giget source (e.g., "gh:myorg/myrepo/my-spec")
|
|
41
80
|
* @param destDir - Destination directory
|
|
42
81
|
* @param strategy - How to handle existing directory: skip, overwrite, or append
|
|
82
|
+
* @param repoSource - Optional giget repo source override. When set, templatePath is
|
|
83
|
+
* treated as relative to this repo. When not set, uses TEMPLATE_REPO.
|
|
84
|
+
* Pass null to use templatePath as a full giget source directly.
|
|
43
85
|
* @returns true if template was downloaded, false if skipped
|
|
44
86
|
*/
|
|
45
|
-
export declare function downloadWithStrategy(templatePath: string, destDir: string, strategy: TemplateStrategy): Promise<boolean>;
|
|
87
|
+
export declare function downloadWithStrategy(templatePath: string, destDir: string, strategy: TemplateStrategy, repoSource?: string | null): Promise<boolean>;
|
|
46
88
|
/**
|
|
47
89
|
* Download a template by ID
|
|
48
90
|
*
|
|
@@ -50,9 +92,20 @@ export declare function downloadWithStrategy(templatePath: string, destDir: stri
|
|
|
50
92
|
* @param templateId - Template ID from the index
|
|
51
93
|
* @param strategy - How to handle existing directory
|
|
52
94
|
* @param template - Optional pre-fetched SpecTemplate to avoid double-fetch
|
|
95
|
+
* @param registry - Optional registry source (parsed). When set, uses the registry's
|
|
96
|
+
* repo as the giget source instead of the default TEMPLATE_REPO.
|
|
53
97
|
* @returns Object with success status and message
|
|
54
98
|
*/
|
|
55
|
-
export declare function downloadTemplateById(cwd: string, templateId: string, strategy: TemplateStrategy, template?: SpecTemplate): Promise<{
|
|
99
|
+
export declare function downloadTemplateById(cwd: string, templateId: string, strategy: TemplateStrategy, template?: SpecTemplate, registry?: RegistrySource): Promise<{
|
|
100
|
+
success: boolean;
|
|
101
|
+
message: string;
|
|
102
|
+
skipped?: boolean;
|
|
103
|
+
}>;
|
|
104
|
+
/**
|
|
105
|
+
* Download a registry source directly to the spec directory (no index.json).
|
|
106
|
+
* Used when the registry source points to a spec directory, not a marketplace.
|
|
107
|
+
*/
|
|
108
|
+
export declare function downloadRegistryDirect(cwd: string, registry: RegistrySource, strategy: TemplateStrategy): Promise<{
|
|
56
109
|
success: boolean;
|
|
57
110
|
message: string;
|
|
58
111
|
skipped?: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"template-fetcher.d.ts","sourceRoot":"","sources":["../../src/utils/template-fetcher.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAWH,eAAO,MAAM,kBAAkB,mFACmD,CAAC;AAYnF,+CAA+C;AAC/C,eAAO,MAAM,QAAQ;IACnB,mDAAmD;;IAEnD,wDAAwD;;CAEhD,CAAC;AAMX,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB;AAOD,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,WAAW,GAAG,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"template-fetcher.d.ts","sourceRoot":"","sources":["../../src/utils/template-fetcher.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAWH,eAAO,MAAM,kBAAkB,mFACmD,CAAC;AAYnF,+CAA+C;AAC/C,eAAO,MAAM,QAAQ;IACnB,mDAAmD;;IAEnD,wDAAwD;;CAEhD,CAAC;AAMX,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB;AAOD,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,WAAW,GAAG,QAAQ,CAAC;AAE/D,MAAM,WAAW,cAAc;IAC7B,mEAAmE;IACnE,QAAQ,EAAE,MAAM,CAAC;IACjB,6CAA6C;IAC7C,IAAI,EAAE,MAAM,CAAC;IACb,gFAAgF;IAChF,MAAM,EAAE,MAAM,CAAC;IACf,yCAAyC;IACzC,GAAG,EAAE,MAAM,CAAC;IACZ,yDAAyD;IACzD,UAAU,EAAE,MAAM,CAAC;IACnB,+CAA+C;IAC/C,WAAW,EAAE,MAAM,CAAC;CACrB;AAcD,eAAO,MAAM,mBAAmB,UAAgC,CAAC;AAEjE;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,cAAc,CAsDlE;AAgCD;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACtC,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,YAAY,EAAE,CAAC,CAezB;AAED;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;IAClE,SAAS,EAAE,YAAY,EAAE,CAAC;IAC1B,UAAU,EAAE,OAAO,CAAC;CACrB,CAAC,CAiBD;AAED;;GAEG;AACH,wBAAsB,YAAY,CAChC,UAAU,EAAE,MAAM,EAClB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAG9B;AAMD;;GAEG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAGxE;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,oBAAoB,CACxC,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,gBAAgB,EAC1B,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,GACzB,OAAO,CAAC,OAAO,CAAC,CA6ElB;AA2BD;;;;;;;;;;GAUG;AACH,wBAAsB,oBAAoB,CACxC,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,gBAAgB,EAC1B,QAAQ,CAAC,EAAE,YAAY,EACvB,QAAQ,CAAC,EAAE,cAAc,GACxB,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC,CA+FnE;AAED;;;GAGG;AACH,wBAAsB,sBAAsB,CAC1C,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,cAAc,EACxB,QAAQ,EAAE,gBAAgB,GACzB,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC,CAgDnE"}
|