@mindfoldhq/trellis 0.3.9 → 0.4.0-beta.1

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 (172) hide show
  1. package/dist/cli/index.js +2 -0
  2. package/dist/cli/index.js.map +1 -1
  3. package/dist/commands/init.d.ts +1 -0
  4. package/dist/commands/init.d.ts.map +1 -1
  5. package/dist/commands/init.js +203 -31
  6. package/dist/commands/init.js.map +1 -1
  7. package/dist/commands/update.d.ts.map +1 -1
  8. package/dist/commands/update.js +154 -6
  9. package/dist/commands/update.js.map +1 -1
  10. package/dist/configurators/workflow.d.ts +6 -2
  11. package/dist/configurators/workflow.d.ts.map +1 -1
  12. package/dist/configurators/workflow.js +88 -58
  13. package/dist/configurators/workflow.js.map +1 -1
  14. package/dist/migrations/index.d.ts +1 -0
  15. package/dist/migrations/index.d.ts.map +1 -1
  16. package/dist/migrations/index.js +2 -0
  17. package/dist/migrations/index.js.map +1 -1
  18. package/dist/migrations/manifests/0.4.0-beta.1.json +228 -0
  19. package/dist/templates/claude/agents/dispatch.md +1 -2
  20. package/dist/templates/claude/agents/implement.md +2 -3
  21. package/dist/templates/claude/commands/trellis/before-dev.md +29 -0
  22. package/dist/templates/claude/commands/trellis/check.md +25 -0
  23. package/dist/templates/claude/commands/trellis/create-command.md +2 -2
  24. package/dist/templates/claude/commands/trellis/onboard.md +13 -13
  25. package/dist/templates/claude/commands/trellis/parallel.md +1 -2
  26. package/dist/templates/claude/commands/trellis/record-session.md +1 -1
  27. package/dist/templates/claude/commands/trellis/start.md +8 -4
  28. package/dist/templates/claude/hooks/inject-subagent-context.py +21 -13
  29. package/dist/templates/claude/hooks/session-start.py +170 -2
  30. package/dist/templates/codex/skills/before-dev/SKILL.md +34 -0
  31. package/dist/templates/codex/skills/check/SKILL.md +30 -0
  32. package/dist/templates/codex/skills/create-command/SKILL.md +2 -2
  33. package/dist/templates/codex/skills/onboard/SKILL.md +11 -11
  34. package/dist/templates/codex/skills/record-session/SKILL.md +1 -1
  35. package/dist/templates/codex/skills/start/SKILL.md +8 -3
  36. package/dist/templates/cursor/commands/trellis-before-dev.md +29 -0
  37. package/dist/templates/cursor/commands/trellis-check.md +25 -0
  38. package/dist/templates/cursor/commands/trellis-create-command.md +2 -2
  39. package/dist/templates/cursor/commands/trellis-onboard.md +13 -13
  40. package/dist/templates/cursor/commands/trellis-record-session.md +1 -1
  41. package/dist/templates/cursor/commands/trellis-start.md +7 -16
  42. package/dist/templates/gemini/commands/trellis/before-dev.toml +33 -0
  43. package/dist/templates/gemini/commands/trellis/check.toml +29 -0
  44. package/dist/templates/gemini/commands/trellis/create-command.toml +2 -2
  45. package/dist/templates/gemini/commands/trellis/onboard.toml +2 -2
  46. package/dist/templates/gemini/commands/trellis/record-session.toml +1 -1
  47. package/dist/templates/gemini/commands/trellis/start.toml +9 -4
  48. package/dist/templates/iflow/agents/dispatch.md +1 -2
  49. package/dist/templates/iflow/agents/implement.md +2 -3
  50. package/dist/templates/iflow/commands/trellis/before-dev.md +29 -0
  51. package/dist/templates/iflow/commands/trellis/check.md +25 -0
  52. package/dist/templates/iflow/commands/trellis/create-command.md +2 -2
  53. package/dist/templates/iflow/commands/trellis/onboard.md +13 -13
  54. package/dist/templates/iflow/commands/trellis/parallel.md +1 -2
  55. package/dist/templates/iflow/commands/trellis/record-session.md +1 -1
  56. package/dist/templates/iflow/commands/trellis/start.md +8 -4
  57. package/dist/templates/iflow/hooks/inject-subagent-context.py +21 -13
  58. package/dist/templates/iflow/hooks/session-start.py +156 -1
  59. package/dist/templates/kilo/workflows/before-dev.md +29 -0
  60. package/dist/templates/kilo/workflows/check.md +25 -0
  61. package/dist/templates/kilo/workflows/create-command.md +2 -2
  62. package/dist/templates/kilo/workflows/onboard.md +13 -13
  63. package/dist/templates/kilo/workflows/parallel.md +1 -2
  64. package/dist/templates/kilo/workflows/record-session.md +1 -1
  65. package/dist/templates/kilo/workflows/start.md +8 -3
  66. package/dist/templates/kiro/skills/before-dev/SKILL.md +34 -0
  67. package/dist/templates/kiro/skills/check/SKILL.md +30 -0
  68. package/dist/templates/kiro/skills/create-command/SKILL.md +2 -2
  69. package/dist/templates/kiro/skills/onboard/SKILL.md +11 -11
  70. package/dist/templates/kiro/skills/record-session/SKILL.md +1 -1
  71. package/dist/templates/kiro/skills/start/SKILL.md +8 -3
  72. package/dist/templates/markdown/spec/backend/script-conventions.md +93 -0
  73. package/dist/templates/opencode/agents/dispatch.md +1 -2
  74. package/dist/templates/opencode/agents/implement.md +2 -2
  75. package/dist/templates/opencode/agents/research.md +1 -2
  76. package/dist/templates/opencode/commands/trellis/before-dev.md +29 -0
  77. package/dist/templates/opencode/commands/trellis/check.md +25 -0
  78. package/dist/templates/opencode/commands/trellis/create-command.md +2 -2
  79. package/dist/templates/opencode/commands/trellis/onboard.md +13 -13
  80. package/dist/templates/opencode/commands/trellis/parallel.md +1 -2
  81. package/dist/templates/opencode/commands/trellis/record-session.md +1 -1
  82. package/dist/templates/opencode/commands/trellis/start.md +8 -3
  83. package/dist/templates/opencode/plugin/inject-subagent-context.js +45 -18
  84. package/dist/templates/opencode/plugin/session-start.js +149 -1
  85. package/dist/templates/qoder/skills/before-dev/SKILL.md +34 -0
  86. package/dist/templates/qoder/skills/check/SKILL.md +30 -0
  87. package/dist/templates/qoder/skills/create-command/SKILL.md +2 -2
  88. package/dist/templates/qoder/skills/onboard/SKILL.md +13 -13
  89. package/dist/templates/qoder/skills/record-session/SKILL.md +1 -1
  90. package/dist/templates/qoder/skills/start/SKILL.md +8 -3
  91. package/dist/templates/trellis/config.yaml +20 -0
  92. package/dist/templates/trellis/index.d.ts +11 -0
  93. package/dist/templates/trellis/index.d.ts.map +1 -1
  94. package/dist/templates/trellis/index.js +22 -0
  95. package/dist/templates/trellis/index.js.map +1 -1
  96. package/dist/templates/trellis/scripts/add_session.py +52 -7
  97. package/dist/templates/trellis/scripts/common/cli_adapter.py +33 -45
  98. package/dist/templates/trellis/scripts/common/config.py +152 -0
  99. package/dist/templates/trellis/scripts/common/git.py +31 -0
  100. package/dist/templates/trellis/scripts/common/git_context.py +23 -586
  101. package/dist/templates/trellis/scripts/common/io.py +37 -0
  102. package/dist/templates/trellis/scripts/common/log.py +45 -0
  103. package/dist/templates/trellis/scripts/common/packages_context.py +233 -0
  104. package/dist/templates/trellis/scripts/common/paths.py +46 -0
  105. package/dist/templates/trellis/scripts/common/phase.py +50 -49
  106. package/dist/templates/trellis/scripts/common/registry.py +41 -72
  107. package/dist/templates/trellis/scripts/common/session_context.py +466 -0
  108. package/dist/templates/trellis/scripts/common/task_context.py +384 -0
  109. package/dist/templates/trellis/scripts/common/task_queue.py +27 -98
  110. package/dist/templates/trellis/scripts/common/task_store.py +534 -0
  111. package/dist/templates/trellis/scripts/common/task_utils.py +96 -6
  112. package/dist/templates/trellis/scripts/common/tasks.py +109 -0
  113. package/dist/templates/trellis/scripts/common/types.py +112 -0
  114. package/dist/templates/trellis/scripts/create_bootstrap.py +31 -26
  115. package/dist/templates/trellis/scripts/hooks/linear_sync.py +243 -0
  116. package/dist/templates/trellis/scripts/multi_agent/_bootstrap.py +17 -0
  117. package/dist/templates/trellis/scripts/multi_agent/cleanup.py +43 -48
  118. package/dist/templates/trellis/scripts/multi_agent/create_pr.py +336 -45
  119. package/dist/templates/trellis/scripts/multi_agent/plan.py +2 -26
  120. package/dist/templates/trellis/scripts/multi_agent/start.py +126 -57
  121. package/dist/templates/trellis/scripts/multi_agent/status.py +12 -753
  122. package/dist/templates/trellis/scripts/multi_agent/status_display.py +542 -0
  123. package/dist/templates/trellis/scripts/multi_agent/status_monitor.py +225 -0
  124. package/dist/templates/trellis/scripts/task.py +50 -975
  125. package/dist/templates/trellis/workflow.md +21 -34
  126. package/dist/types/migration.d.ts +3 -1
  127. package/dist/types/migration.d.ts.map +1 -1
  128. package/dist/utils/project-detector.d.ts +23 -0
  129. package/dist/utils/project-detector.d.ts.map +1 -1
  130. package/dist/utils/project-detector.js +364 -0
  131. package/dist/utils/project-detector.js.map +1 -1
  132. package/dist/utils/template-fetcher.d.ts +2 -2
  133. package/dist/utils/template-fetcher.d.ts.map +1 -1
  134. package/dist/utils/template-fetcher.js +5 -5
  135. package/dist/utils/template-fetcher.js.map +1 -1
  136. package/package.json +1 -1
  137. package/dist/templates/claude/commands/trellis/before-backend-dev.md +0 -13
  138. package/dist/templates/claude/commands/trellis/before-frontend-dev.md +0 -13
  139. package/dist/templates/claude/commands/trellis/check-backend.md +0 -13
  140. package/dist/templates/claude/commands/trellis/check-frontend.md +0 -13
  141. package/dist/templates/codex/skills/before-backend-dev/SKILL.md +0 -18
  142. package/dist/templates/codex/skills/before-frontend-dev/SKILL.md +0 -18
  143. package/dist/templates/codex/skills/check-backend/SKILL.md +0 -18
  144. package/dist/templates/codex/skills/check-frontend/SKILL.md +0 -18
  145. package/dist/templates/cursor/commands/trellis-before-backend-dev.md +0 -13
  146. package/dist/templates/cursor/commands/trellis-before-frontend-dev.md +0 -13
  147. package/dist/templates/cursor/commands/trellis-check-backend.md +0 -13
  148. package/dist/templates/cursor/commands/trellis-check-frontend.md +0 -13
  149. package/dist/templates/gemini/commands/trellis/before-backend-dev.toml +0 -17
  150. package/dist/templates/gemini/commands/trellis/before-frontend-dev.toml +0 -17
  151. package/dist/templates/gemini/commands/trellis/check-backend.toml +0 -17
  152. package/dist/templates/gemini/commands/trellis/check-frontend.toml +0 -17
  153. package/dist/templates/iflow/commands/trellis/before-backend-dev.md +0 -13
  154. package/dist/templates/iflow/commands/trellis/before-frontend-dev.md +0 -13
  155. package/dist/templates/iflow/commands/trellis/check-backend.md +0 -13
  156. package/dist/templates/iflow/commands/trellis/check-frontend.md +0 -13
  157. package/dist/templates/kilo/workflows/before-backend-dev.md +0 -13
  158. package/dist/templates/kilo/workflows/before-frontend-dev.md +0 -13
  159. package/dist/templates/kilo/workflows/check-backend.md +0 -13
  160. package/dist/templates/kilo/workflows/check-frontend.md +0 -13
  161. package/dist/templates/kiro/skills/before-backend-dev/SKILL.md +0 -18
  162. package/dist/templates/kiro/skills/before-frontend-dev/SKILL.md +0 -18
  163. package/dist/templates/kiro/skills/check-backend/SKILL.md +0 -18
  164. package/dist/templates/kiro/skills/check-frontend/SKILL.md +0 -18
  165. package/dist/templates/opencode/commands/trellis/before-backend-dev.md +0 -13
  166. package/dist/templates/opencode/commands/trellis/before-frontend-dev.md +0 -13
  167. package/dist/templates/opencode/commands/trellis/check-backend.md +0 -13
  168. package/dist/templates/opencode/commands/trellis/check-frontend.md +0 -13
  169. package/dist/templates/qoder/skills/before-backend-dev/SKILL.md +0 -18
  170. package/dist/templates/qoder/skills/before-frontend-dev/SKILL.md +0 -18
  171. package/dist/templates/qoder/skills/check-backend/SKILL.md +0 -18
  172. package/dist/templates/qoder/skills/check-frontend/SKILL.md +0 -18
@@ -0,0 +1,384 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Task JSONL context management.
4
+
5
+ Provides:
6
+ cmd_init_context - Initialize JSONL context files for a task
7
+ cmd_add_context - Add entry to JSONL context file
8
+ cmd_validate - Validate JSONL context files
9
+ cmd_list_context - List JSONL context entries
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import json
16
+ import sys
17
+ from pathlib import Path
18
+
19
+ from .cli_adapter import get_cli_adapter_auto
20
+ from .config import (
21
+ get_packages,
22
+ is_monorepo,
23
+ resolve_package,
24
+ validate_package,
25
+ )
26
+ from .io import read_json, write_json
27
+ from .log import Colors, colored
28
+ from .paths import (
29
+ DIR_SPEC,
30
+ DIR_WORKFLOW,
31
+ FILE_TASK_JSON,
32
+ get_repo_root,
33
+ )
34
+ from .task_utils import resolve_task_dir
35
+
36
+
37
+ # =============================================================================
38
+ # JSONL Default Content Generators
39
+ # =============================================================================
40
+
41
+ def get_implement_base() -> list[dict]:
42
+ """Get base implement context entries."""
43
+ return [
44
+ {"file": f"{DIR_WORKFLOW}/workflow.md", "reason": "Project workflow and conventions"},
45
+ ]
46
+
47
+
48
+ def get_implement_backend(package: str | None = None) -> list[dict]:
49
+ """Get backend implement context entries."""
50
+ spec_base = f"{DIR_SPEC}/{package}" if package else DIR_SPEC
51
+ return [
52
+ {"file": f"{DIR_WORKFLOW}/{spec_base}/backend/index.md", "reason": "Backend development guide"},
53
+ ]
54
+
55
+
56
+ def get_implement_frontend(package: str | None = None) -> list[dict]:
57
+ """Get frontend implement context entries."""
58
+ spec_base = f"{DIR_SPEC}/{package}" if package else DIR_SPEC
59
+ return [
60
+ {"file": f"{DIR_WORKFLOW}/{spec_base}/frontend/index.md", "reason": "Frontend development guide"},
61
+ ]
62
+
63
+
64
+ def get_check_context(repo_root: Path) -> list[dict]:
65
+ """Get check context entries."""
66
+ adapter = get_cli_adapter_auto(repo_root)
67
+
68
+ entries = [
69
+ {"file": adapter.get_trellis_command_path("finish-work"), "reason": "Finish work checklist"},
70
+ {"file": adapter.get_trellis_command_path("check"), "reason": "Code quality check spec"},
71
+ ]
72
+
73
+ return entries
74
+
75
+
76
+ def get_debug_context(repo_root: Path) -> list[dict]:
77
+ """Get debug context entries."""
78
+ adapter = get_cli_adapter_auto(repo_root)
79
+
80
+ entries: list[dict] = [
81
+ {"file": adapter.get_trellis_command_path("check"), "reason": "Code quality check spec"},
82
+ ]
83
+
84
+ return entries
85
+
86
+
87
+ def _write_jsonl(path: Path, entries: list[dict]) -> None:
88
+ """Write entries to JSONL file."""
89
+ lines = [json.dumps(entry, ensure_ascii=False) for entry in entries]
90
+ path.write_text("\n".join(lines) + "\n", encoding="utf-8")
91
+
92
+
93
+ # =============================================================================
94
+ # Command: init-context
95
+ # =============================================================================
96
+
97
+ def cmd_init_context(args: argparse.Namespace) -> int:
98
+ """Initialize JSONL context files for a task."""
99
+ repo_root = get_repo_root()
100
+ target_dir = resolve_task_dir(args.dir, repo_root)
101
+ dev_type = args.type
102
+
103
+ if not dev_type:
104
+ print(colored("Error: Missing arguments", Colors.RED))
105
+ print("Usage: python3 task.py init-context <task-dir> <dev_type>")
106
+ print(" dev_type: backend | frontend | fullstack | test | docs")
107
+ return 1
108
+
109
+ if not target_dir.is_dir():
110
+ print(colored(f"Error: Directory not found: {target_dir}", Colors.RED))
111
+ return 1
112
+
113
+ # Resolve package: --package CLI → task.json.package → default_package
114
+ cli_package: str | None = getattr(args, "package", None)
115
+ package: str | None = None
116
+ if not is_monorepo(repo_root):
117
+ # Single-repo: ignore --package, no package prefix
118
+ if cli_package:
119
+ print(colored("Warning: --package ignored in single-repo project", Colors.YELLOW), file=sys.stderr)
120
+ elif cli_package:
121
+ if not validate_package(cli_package, repo_root):
122
+ packages = get_packages(repo_root)
123
+ available = ", ".join(sorted(packages.keys())) if packages else "(none)"
124
+ print(colored(f"Error: unknown package '{cli_package}'. Available: {available}", Colors.RED), file=sys.stderr)
125
+ return 1
126
+ package = cli_package
127
+ else:
128
+ # Read task.json.package as inferred source
129
+ task_json_path = target_dir / FILE_TASK_JSON
130
+ task_pkg_value = None
131
+ if task_json_path.is_file():
132
+ task_data = read_json(task_json_path)
133
+ if isinstance(task_data, dict):
134
+ task_pkg_value = task_data.get("package")
135
+ # Only pass string values to resolve_package (guard against malformed JSON)
136
+ task_package = task_pkg_value if isinstance(task_pkg_value, str) else None
137
+ package = resolve_package(task_package=task_package, repo_root=repo_root)
138
+
139
+ # Monorepo fallback prohibition
140
+ if package is None:
141
+ packages = get_packages(repo_root)
142
+ available = ", ".join(sorted(packages.keys())) if packages else "(none)"
143
+ print(colored(
144
+ f"Error: monorepo project requires --package (or set default_package in config.yaml). Available: {available}",
145
+ Colors.RED,
146
+ ), file=sys.stderr)
147
+ return 1
148
+
149
+ print(colored("=== Initializing Agent Context Files ===", Colors.BLUE))
150
+ print(f"Target dir: {target_dir}")
151
+ print(f"Dev type: {dev_type}")
152
+ if package:
153
+ print(f"Package: {package}")
154
+ print()
155
+
156
+ # implement.jsonl
157
+ print(colored("Creating implement.jsonl...", Colors.CYAN))
158
+ implement_entries = get_implement_base()
159
+ if dev_type in ("backend", "test"):
160
+ implement_entries.extend(get_implement_backend(package))
161
+ elif dev_type == "frontend":
162
+ implement_entries.extend(get_implement_frontend(package))
163
+ elif dev_type == "fullstack":
164
+ implement_entries.extend(get_implement_backend(package))
165
+ implement_entries.extend(get_implement_frontend(package))
166
+
167
+ implement_file = target_dir / "implement.jsonl"
168
+ _write_jsonl(implement_file, implement_entries)
169
+ print(f" {colored('✓', Colors.GREEN)} {len(implement_entries)} entries")
170
+
171
+ # check.jsonl
172
+ print(colored("Creating check.jsonl...", Colors.CYAN))
173
+ check_entries = get_check_context(repo_root)
174
+ check_file = target_dir / "check.jsonl"
175
+ _write_jsonl(check_file, check_entries)
176
+ print(f" {colored('✓', Colors.GREEN)} {len(check_entries)} entries")
177
+
178
+ # debug.jsonl
179
+ print(colored("Creating debug.jsonl...", Colors.CYAN))
180
+ debug_entries = get_debug_context(repo_root)
181
+ debug_file = target_dir / "debug.jsonl"
182
+ _write_jsonl(debug_file, debug_entries)
183
+ print(f" {colored('✓', Colors.GREEN)} {len(debug_entries)} entries")
184
+
185
+ # Update task.json dev_type and package
186
+ task_json_path = target_dir / FILE_TASK_JSON
187
+ if task_json_path.is_file():
188
+ task_data = read_json(task_json_path)
189
+ if isinstance(task_data, dict):
190
+ task_data["dev_type"] = dev_type
191
+ task_data["package"] = package # Always sync to match resolved value
192
+ write_json(task_json_path, task_data)
193
+
194
+ print()
195
+ print(colored("✓ All context files created", Colors.GREEN))
196
+ print()
197
+ print(colored("Next steps:", Colors.BLUE))
198
+ print(" 1. Add task-specific specs: python3 task.py add-context <dir> <jsonl> <path>")
199
+ print(" 2. Set as current: python3 task.py start <dir>")
200
+
201
+ return 0
202
+
203
+
204
+ # =============================================================================
205
+ # Command: add-context
206
+ # =============================================================================
207
+
208
+ def cmd_add_context(args: argparse.Namespace) -> int:
209
+ """Add entry to JSONL context file."""
210
+ repo_root = get_repo_root()
211
+ target_dir = resolve_task_dir(args.dir, repo_root)
212
+
213
+ jsonl_name = args.file
214
+ path = args.path
215
+ reason = args.reason or "Added manually"
216
+
217
+ if not target_dir.is_dir():
218
+ print(colored(f"Error: Directory not found: {target_dir}", Colors.RED))
219
+ return 1
220
+
221
+ # Support shorthand
222
+ if not jsonl_name.endswith(".jsonl"):
223
+ jsonl_name = f"{jsonl_name}.jsonl"
224
+
225
+ jsonl_file = target_dir / jsonl_name
226
+ full_path = repo_root / path
227
+
228
+ entry_type = "file"
229
+ if full_path.is_dir():
230
+ entry_type = "directory"
231
+ if not path.endswith("/"):
232
+ path = f"{path}/"
233
+ elif not full_path.is_file():
234
+ print(colored(f"Error: Path not found: {path}", Colors.RED))
235
+ return 1
236
+
237
+ # Check if already exists
238
+ if jsonl_file.is_file():
239
+ content = jsonl_file.read_text(encoding="utf-8")
240
+ if f'"{path}"' in content:
241
+ print(colored(f"Warning: Entry already exists for {path}", Colors.YELLOW))
242
+ return 0
243
+
244
+ # Add entry
245
+ entry: dict
246
+ if entry_type == "directory":
247
+ entry = {"file": path, "type": "directory", "reason": reason}
248
+ else:
249
+ entry = {"file": path, "reason": reason}
250
+
251
+ with jsonl_file.open("a", encoding="utf-8") as f:
252
+ f.write(json.dumps(entry, ensure_ascii=False) + "\n")
253
+
254
+ print(colored(f"Added {entry_type}: {path}", Colors.GREEN))
255
+ return 0
256
+
257
+
258
+ # =============================================================================
259
+ # Command: validate
260
+ # =============================================================================
261
+
262
+ def cmd_validate(args: argparse.Namespace) -> int:
263
+ """Validate JSONL context files."""
264
+ repo_root = get_repo_root()
265
+ target_dir = resolve_task_dir(args.dir, repo_root)
266
+
267
+ if not target_dir.is_dir():
268
+ print(colored("Error: task directory required", Colors.RED))
269
+ return 1
270
+
271
+ print(colored("=== Validating Context Files ===", Colors.BLUE))
272
+ print(f"Target dir: {target_dir}")
273
+ print()
274
+
275
+ total_errors = 0
276
+ for jsonl_name in ["implement.jsonl", "check.jsonl", "debug.jsonl"]:
277
+ jsonl_file = target_dir / jsonl_name
278
+ errors = _validate_jsonl(jsonl_file, repo_root)
279
+ total_errors += errors
280
+
281
+ print()
282
+ if total_errors == 0:
283
+ print(colored("✓ All validations passed", Colors.GREEN))
284
+ return 0
285
+ else:
286
+ print(colored(f"✗ Validation failed ({total_errors} errors)", Colors.RED))
287
+ return 1
288
+
289
+
290
+ def _validate_jsonl(jsonl_file: Path, repo_root: Path) -> int:
291
+ """Validate a single JSONL file."""
292
+ file_name = jsonl_file.name
293
+ errors = 0
294
+
295
+ if not jsonl_file.is_file():
296
+ print(f" {colored(f'{file_name}: not found (skipped)', Colors.YELLOW)}")
297
+ return 0
298
+
299
+ line_num = 0
300
+ for line in jsonl_file.read_text(encoding="utf-8").splitlines():
301
+ line_num += 1
302
+ if not line.strip():
303
+ continue
304
+
305
+ try:
306
+ data = json.loads(line)
307
+ except json.JSONDecodeError:
308
+ print(f" {colored(f'{file_name}:{line_num}: Invalid JSON', Colors.RED)}")
309
+ errors += 1
310
+ continue
311
+
312
+ file_path = data.get("file")
313
+ entry_type = data.get("type", "file")
314
+
315
+ if not file_path:
316
+ print(f" {colored(f'{file_name}:{line_num}: Missing file field', Colors.RED)}")
317
+ errors += 1
318
+ continue
319
+
320
+ full_path = repo_root / file_path
321
+ if entry_type == "directory":
322
+ if not full_path.is_dir():
323
+ print(f" {colored(f'{file_name}:{line_num}: Directory not found: {file_path}', Colors.RED)}")
324
+ errors += 1
325
+ else:
326
+ if not full_path.is_file():
327
+ print(f" {colored(f'{file_name}:{line_num}: File not found: {file_path}', Colors.RED)}")
328
+ errors += 1
329
+
330
+ if errors == 0:
331
+ print(f" {colored(f'{file_name}: ✓ ({line_num} entries)', Colors.GREEN)}")
332
+ else:
333
+ print(f" {colored(f'{file_name}: ✗ ({errors} errors)', Colors.RED)}")
334
+
335
+ return errors
336
+
337
+
338
+ # =============================================================================
339
+ # Command: list-context
340
+ # =============================================================================
341
+
342
+ def cmd_list_context(args: argparse.Namespace) -> int:
343
+ """List JSONL context entries."""
344
+ repo_root = get_repo_root()
345
+ target_dir = resolve_task_dir(args.dir, repo_root)
346
+
347
+ if not target_dir.is_dir():
348
+ print(colored("Error: task directory required", Colors.RED))
349
+ return 1
350
+
351
+ print(colored("=== Context Files ===", Colors.BLUE))
352
+ print()
353
+
354
+ for jsonl_name in ["implement.jsonl", "check.jsonl", "debug.jsonl"]:
355
+ jsonl_file = target_dir / jsonl_name
356
+ if not jsonl_file.is_file():
357
+ continue
358
+
359
+ print(colored(f"[{jsonl_name}]", Colors.CYAN))
360
+
361
+ count = 0
362
+ for line in jsonl_file.read_text(encoding="utf-8").splitlines():
363
+ if not line.strip():
364
+ continue
365
+
366
+ try:
367
+ data = json.loads(line)
368
+ except json.JSONDecodeError:
369
+ continue
370
+
371
+ count += 1
372
+ file_path = data.get("file", "?")
373
+ entry_type = data.get("type", "file")
374
+ reason = data.get("reason", "-")
375
+
376
+ if entry_type == "directory":
377
+ print(f" {colored(f'{count}.', Colors.GREEN)} [DIR] {file_path}")
378
+ else:
379
+ print(f" {colored(f'{count}.', Colors.GREEN)} {file_path}")
380
+ print(f" {colored('→', Colors.YELLOW)} {reason}")
381
+
382
+ print()
383
+
384
+ return 0
@@ -12,23 +12,32 @@ Provides:
12
12
 
13
13
  from __future__ import annotations
14
14
 
15
- import json
16
15
  from pathlib import Path
17
16
 
18
17
  from .paths import (
19
- FILE_TASK_JSON,
20
18
  get_repo_root,
21
19
  get_developer,
22
20
  get_tasks_dir,
23
21
  )
22
+ from .tasks import iter_active_tasks
24
23
 
25
24
 
26
- def _read_json_file(path: Path) -> dict | None:
27
- """Read and parse a JSON file."""
28
- try:
29
- return json.loads(path.read_text(encoding="utf-8"))
30
- except (FileNotFoundError, json.JSONDecodeError, OSError):
31
- return None
25
+ # =============================================================================
26
+ # Internal helper
27
+ # =============================================================================
28
+
29
+ def _task_to_dict(t) -> dict:
30
+ """Convert TaskInfo to the dict format callers expect."""
31
+ return {
32
+ "priority": t.priority,
33
+ "id": t.raw.get("id", ""),
34
+ "title": t.title,
35
+ "status": t.status,
36
+ "assignee": t.assignee or "-",
37
+ "dir": t.dir_name,
38
+ "children": list(t.children),
39
+ "parent": t.parent,
40
+ }
32
41
 
33
42
 
34
43
  # =============================================================================
@@ -54,41 +63,10 @@ def list_tasks_by_status(
54
63
  tasks_dir = get_tasks_dir(repo_root)
55
64
  results = []
56
65
 
57
- if not tasks_dir.is_dir():
58
- return results
59
-
60
- for d in tasks_dir.iterdir():
61
- if not d.is_dir() or d.name == "archive":
62
- continue
63
-
64
- task_json = d / FILE_TASK_JSON
65
- if not task_json.is_file():
66
- continue
67
-
68
- data = _read_json_file(task_json)
69
- if not data:
66
+ for t in iter_active_tasks(tasks_dir):
67
+ if filter_status and t.status != filter_status:
70
68
  continue
71
-
72
- task_id = data.get("id", "")
73
- title = data.get("title") or data.get("name", "")
74
- priority = data.get("priority", "P2")
75
- status = data.get("status", "planning")
76
- assignee = data.get("assignee", "-")
77
-
78
- # Apply filter
79
- if filter_status and status != filter_status:
80
- continue
81
-
82
- results.append({
83
- "priority": priority,
84
- "id": task_id,
85
- "title": title,
86
- "status": status,
87
- "assignee": assignee,
88
- "dir": d.name,
89
- "children": data.get("children", []),
90
- "parent": data.get("parent"),
91
- })
69
+ results.append(_task_to_dict(t))
92
70
 
93
71
  return results
94
72
 
@@ -126,46 +104,12 @@ def list_tasks_by_assignee(
126
104
  tasks_dir = get_tasks_dir(repo_root)
127
105
  results = []
128
106
 
129
- if not tasks_dir.is_dir():
130
- return results
131
-
132
- for d in tasks_dir.iterdir():
133
- if not d.is_dir() or d.name == "archive":
134
- continue
135
-
136
- task_json = d / FILE_TASK_JSON
137
- if not task_json.is_file():
107
+ for t in iter_active_tasks(tasks_dir):
108
+ if (t.assignee or "-") != assignee:
138
109
  continue
139
-
140
- data = _read_json_file(task_json)
141
- if not data:
142
- continue
143
-
144
- task_assignee = data.get("assignee", "-")
145
-
146
- # Apply assignee filter
147
- if task_assignee != assignee:
110
+ if filter_status and t.status != filter_status:
148
111
  continue
149
-
150
- task_id = data.get("id", "")
151
- title = data.get("title") or data.get("name", "")
152
- priority = data.get("priority", "P2")
153
- status = data.get("status", "planning")
154
-
155
- # Apply status filter
156
- if filter_status and status != filter_status:
157
- continue
158
-
159
- results.append({
160
- "priority": priority,
161
- "id": task_id,
162
- "title": title,
163
- "status": status,
164
- "assignee": task_assignee,
165
- "dir": d.name,
166
- "children": data.get("children", []),
167
- "parent": data.get("parent"),
168
- })
112
+ results.append(_task_to_dict(t))
169
113
 
170
114
  return results
171
115
 
@@ -211,24 +155,9 @@ def get_task_stats(repo_root: Path | None = None) -> dict[str, int]:
211
155
  tasks_dir = get_tasks_dir(repo_root)
212
156
  stats = {"P0": 0, "P1": 0, "P2": 0, "P3": 0, "Total": 0}
213
157
 
214
- if not tasks_dir.is_dir():
215
- return stats
216
-
217
- for d in tasks_dir.iterdir():
218
- if not d.is_dir() or d.name == "archive":
219
- continue
220
-
221
- task_json = d / FILE_TASK_JSON
222
- if not task_json.is_file():
223
- continue
224
-
225
- data = _read_json_file(task_json)
226
- if not data:
227
- continue
228
-
229
- priority = data.get("priority", "P2")
230
- if priority in stats:
231
- stats[priority] += 1
158
+ for t in iter_active_tasks(tasks_dir):
159
+ if t.priority in stats:
160
+ stats[t.priority] += 1
232
161
  stats["Total"] += 1
233
162
 
234
163
  return stats