@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.
Files changed (57) hide show
  1. package/README.md +6 -4
  2. package/dist/cli/index.js +1 -0
  3. package/dist/cli/index.js.map +1 -1
  4. package/dist/commands/init.d.ts +1 -0
  5. package/dist/commands/init.d.ts.map +1 -1
  6. package/dist/commands/init.js +243 -49
  7. package/dist/commands/init.js.map +1 -1
  8. package/dist/commands/update.d.ts.map +1 -1
  9. package/dist/commands/update.js +3 -0
  10. package/dist/commands/update.js.map +1 -1
  11. package/dist/migrations/manifests/0.3.4.json +1 -1
  12. package/dist/migrations/manifests/0.3.5.json +9 -0
  13. package/dist/migrations/manifests/0.3.6.json +9 -0
  14. package/dist/templates/claude/commands/trellis/brainstorm.md +13 -0
  15. package/dist/templates/claude/commands/trellis/record-session.md +4 -1
  16. package/dist/templates/claude/commands/trellis/start.md +4 -0
  17. package/dist/templates/claude/hooks/inject-subagent-context.py +1 -1
  18. package/dist/templates/claude/settings.json +10 -0
  19. package/dist/templates/codex/skills/brainstorm/SKILL.md +13 -0
  20. package/dist/templates/codex/skills/record-session/SKILL.md +4 -1
  21. package/dist/templates/codex/skills/start/SKILL.md +4 -0
  22. package/dist/templates/cursor/commands/trellis-brainstorm.md +13 -0
  23. package/dist/templates/cursor/commands/trellis-record-session.md +4 -1
  24. package/dist/templates/cursor/commands/trellis-start.md +4 -0
  25. package/dist/templates/gemini/commands/trellis/brainstorm.toml +15 -0
  26. package/dist/templates/gemini/commands/trellis/record-session.toml +4 -1
  27. package/dist/templates/gemini/commands/trellis/start.toml +4 -0
  28. package/dist/templates/iflow/commands/trellis/brainstorm.md +13 -0
  29. package/dist/templates/iflow/commands/trellis/record-session.md +4 -1
  30. package/dist/templates/iflow/commands/trellis/start.md +4 -0
  31. package/dist/templates/iflow/hooks/inject-subagent-context.py +1 -1
  32. package/dist/templates/kilo/workflows/brainstorm.md +13 -0
  33. package/dist/templates/kilo/workflows/record-session.md +4 -1
  34. package/dist/templates/kilo/workflows/start.md +4 -0
  35. package/dist/templates/kiro/skills/brainstorm/SKILL.md +13 -0
  36. package/dist/templates/kiro/skills/record-session/SKILL.md +4 -1
  37. package/dist/templates/kiro/skills/start/SKILL.md +4 -0
  38. package/dist/templates/markdown/spec/backend/directory-structure.md +292 -0
  39. package/dist/templates/markdown/spec/backend/script-conventions.md +220 -38
  40. package/dist/templates/opencode/commands/trellis/brainstorm.md +13 -0
  41. package/dist/templates/opencode/commands/trellis/record-session.md +4 -1
  42. package/dist/templates/opencode/commands/trellis/start.md +4 -0
  43. package/dist/templates/qoder/skills/brainstorm/SKILL.md +13 -0
  44. package/dist/templates/qoder/skills/record-session/SKILL.md +4 -1
  45. package/dist/templates/qoder/skills/start/SKILL.md +4 -0
  46. package/dist/templates/trellis/config.yaml +18 -0
  47. package/dist/templates/trellis/scripts/common/config.py +20 -0
  48. package/dist/templates/trellis/scripts/common/git_context.py +160 -12
  49. package/dist/templates/trellis/scripts/common/task_queue.py +4 -0
  50. package/dist/templates/trellis/scripts/common/worktree.py +78 -11
  51. package/dist/templates/trellis/scripts/create_bootstrap.py +3 -0
  52. package/dist/templates/trellis/scripts/task.py +312 -17
  53. package/dist/utils/template-fetcher.d.ts +57 -4
  54. package/dist/utils/template-fetcher.d.ts.map +1 -1
  55. package/dist/utils/template-fetcher.js +179 -10
  56. package/dist/utils/template-fetcher.js.map +1 -1
  57. 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
- count = 0
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
- relative_path = f"{DIR_WORKFLOW}/{DIR_TASKS}/{dir_name}"
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
- # Apply --mine filter
735
- if filter_mine and assignee != developer:
736
- continue
976
+ all_tasks[dir_name] = {
977
+ "status": status,
978
+ "assignee": assignee,
979
+ "children": children,
980
+ "parent": parent,
981
+ }
737
982
 
738
- # Apply --status filter
739
- if filter_status and status != filter_status:
740
- continue
983
+ # Second pass: display tasks hierarchically
984
+ count = 0
741
985
 
742
- marker = ""
743
- if relative_path == current_task:
744
- marker = f" {colored('<- current', Colors.GREEN)}"
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
- if filter_mine:
747
- print(f" - {dir_name}/ ({status}){marker}")
748
- else:
749
- print(f" - {dir_name}/ ({status}) [{colored(assignee, Colors.CYAN)}]{marker}")
750
- count += 1
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 fetchTemplateIndex(): Promise<SpecTemplate[]>;
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;AAgC/D;;;GAGG;AACH,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC,CAclE;AAED;;GAEG;AACH,wBAAsB,YAAY,CAChC,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAG9B;AAMD;;GAEG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAGxE;AAED;;;;;;;GAOG;AACH,wBAAsB,oBAAoB,CACxC,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,gBAAgB,GACzB,OAAO,CAAC,OAAO,CAAC,CAuElB;AA2BD;;;;;;;;GAQG;AACH,wBAAsB,oBAAoB,CACxC,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,gBAAgB,EAC1B,QAAQ,CAAC,EAAE,YAAY,GACtB,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC,CAgEnE"}
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"}