task_context.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. #!/usr/bin/env python3
  2. """
  3. Task JSONL context management.
  4. Provides:
  5. cmd_add_context - Add entry to JSONL context file
  6. cmd_validate - Validate JSONL context files
  7. cmd_list_context - List JSONL context entries
  8. Note:
  9. ``cmd_init_context`` was removed in v0.5.0-beta.12. JSONL context files
  10. are now seeded at ``task.py create`` time with a self-describing
  11. ``_example`` line; the AI agent curates real entries during Phase 1.3 of
  12. the workflow. See ``.trellis/workflow.md`` Phase 1.3 for the current
  13. instructions.
  14. """
  15. from __future__ import annotations
  16. import argparse
  17. import json
  18. from pathlib import Path
  19. from .log import Colors, colored
  20. from .paths import get_repo_root
  21. from .task_utils import resolve_task_dir
  22. # =============================================================================
  23. # Command: add-context
  24. # =============================================================================
  25. def cmd_add_context(args: argparse.Namespace) -> int:
  26. """Add entry to JSONL context file."""
  27. repo_root = get_repo_root()
  28. target_dir = resolve_task_dir(args.dir, repo_root)
  29. jsonl_name = args.file
  30. path = args.path
  31. reason = args.reason or "Added manually"
  32. if not target_dir.is_dir():
  33. print(colored(f"Error: Directory not found: {target_dir}", Colors.RED))
  34. return 1
  35. # Support shorthand
  36. if not jsonl_name.endswith(".jsonl"):
  37. jsonl_name = f"{jsonl_name}.jsonl"
  38. jsonl_file = target_dir / jsonl_name
  39. full_path = repo_root / path
  40. entry_type = "file"
  41. if full_path.is_dir():
  42. entry_type = "directory"
  43. if not path.endswith("/"):
  44. path = f"{path}/"
  45. elif not full_path.is_file():
  46. print(colored(f"Error: Path not found: {path}", Colors.RED))
  47. return 1
  48. # Check if already exists
  49. if jsonl_file.is_file():
  50. content = jsonl_file.read_text(encoding="utf-8")
  51. if f'"{path}"' in content:
  52. print(colored(f"Warning: Entry already exists for {path}", Colors.YELLOW))
  53. return 0
  54. # Add entry
  55. entry: dict
  56. if entry_type == "directory":
  57. entry = {"file": path, "type": "directory", "reason": reason}
  58. else:
  59. entry = {"file": path, "reason": reason}
  60. with jsonl_file.open("a", encoding="utf-8") as f:
  61. f.write(json.dumps(entry, ensure_ascii=False) + "\n")
  62. print(colored(f"Added {entry_type}: {path}", Colors.GREEN))
  63. return 0
  64. # =============================================================================
  65. # Command: validate
  66. # =============================================================================
  67. def cmd_validate(args: argparse.Namespace) -> int:
  68. """Validate JSONL context files."""
  69. repo_root = get_repo_root()
  70. target_dir = resolve_task_dir(args.dir, repo_root)
  71. if not target_dir.is_dir():
  72. print(colored("Error: task directory required", Colors.RED))
  73. return 1
  74. print(colored("=== Validating Context Files ===", Colors.BLUE))
  75. print(f"Target dir: {target_dir}")
  76. print()
  77. total_errors = 0
  78. for jsonl_name in ["implement.jsonl", "check.jsonl"]:
  79. jsonl_file = target_dir / jsonl_name
  80. errors = _validate_jsonl(jsonl_file, repo_root)
  81. total_errors += errors
  82. print()
  83. if total_errors == 0:
  84. print(colored("✓ All validations passed", Colors.GREEN))
  85. return 0
  86. else:
  87. print(colored(f"✗ Validation failed ({total_errors} errors)", Colors.RED))
  88. return 1
  89. def _validate_jsonl(jsonl_file: Path, repo_root: Path) -> int:
  90. """Validate a single JSONL file.
  91. Seed rows (no ``file`` field — typically ``{"_example": "..."}``) are
  92. skipped silently; they are self-describing comments, not real entries.
  93. """
  94. file_name = jsonl_file.name
  95. errors = 0
  96. if not jsonl_file.is_file():
  97. print(f" {colored(f'{file_name}: not found (skipped)', Colors.YELLOW)}")
  98. return 0
  99. line_num = 0
  100. real_entries = 0
  101. for line in jsonl_file.read_text(encoding="utf-8").splitlines():
  102. line_num += 1
  103. if not line.strip():
  104. continue
  105. try:
  106. data = json.loads(line)
  107. except json.JSONDecodeError:
  108. print(f" {colored(f'{file_name}:{line_num}: Invalid JSON', Colors.RED)}")
  109. errors += 1
  110. continue
  111. file_path = data.get("file")
  112. entry_type = data.get("type", "file")
  113. if not file_path:
  114. # Seed / comment row — skip silently
  115. continue
  116. real_entries += 1
  117. full_path = repo_root / file_path
  118. if entry_type == "directory":
  119. if not full_path.is_dir():
  120. print(f" {colored(f'{file_name}:{line_num}: Directory not found: {file_path}', Colors.RED)}")
  121. errors += 1
  122. else:
  123. if not full_path.is_file():
  124. print(f" {colored(f'{file_name}:{line_num}: File not found: {file_path}', Colors.RED)}")
  125. errors += 1
  126. if errors == 0:
  127. print(f" {colored(f'{file_name}: ✓ ({real_entries} entries)', Colors.GREEN)}")
  128. else:
  129. print(f" {colored(f'{file_name}: ✗ ({errors} errors)', Colors.RED)}")
  130. return errors
  131. # =============================================================================
  132. # Command: list-context
  133. # =============================================================================
  134. def cmd_list_context(args: argparse.Namespace) -> int:
  135. """List JSONL context entries."""
  136. repo_root = get_repo_root()
  137. target_dir = resolve_task_dir(args.dir, repo_root)
  138. if not target_dir.is_dir():
  139. print(colored("Error: task directory required", Colors.RED))
  140. return 1
  141. print(colored("=== Context Files ===", Colors.BLUE))
  142. print()
  143. for jsonl_name in ["implement.jsonl", "check.jsonl"]:
  144. jsonl_file = target_dir / jsonl_name
  145. if not jsonl_file.is_file():
  146. continue
  147. print(colored(f"[{jsonl_name}]", Colors.CYAN))
  148. count = 0
  149. seed_only = True
  150. for line in jsonl_file.read_text(encoding="utf-8").splitlines():
  151. if not line.strip():
  152. continue
  153. try:
  154. data = json.loads(line)
  155. except json.JSONDecodeError:
  156. continue
  157. file_path = data.get("file")
  158. if not file_path:
  159. # Seed / comment row — don't count as a real entry
  160. continue
  161. seed_only = False
  162. count += 1
  163. entry_type = data.get("type", "file")
  164. reason = data.get("reason", "-")
  165. if entry_type == "directory":
  166. print(f" {colored(f'{count}.', Colors.GREEN)} [DIR] {file_path}")
  167. else:
  168. print(f" {colored(f'{count}.', Colors.GREEN)} {file_path}")
  169. print(f" {colored('→', Colors.YELLOW)} {reason}")
  170. if seed_only:
  171. print(f" {colored('(no curated entries yet — only seed row)', Colors.YELLOW)}")
  172. print()
  173. return 0