@mindfoldhq/trellis 0.3.5 → 0.3.7

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 (65) hide show
  1. package/README.md +17 -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 +12 -0
  9. package/dist/commands/update.d.ts.map +1 -1
  10. package/dist/commands/update.js +92 -2
  11. package/dist/commands/update.js.map +1 -1
  12. package/dist/migrations/index.d.ts.map +1 -1
  13. package/dist/migrations/index.js +3 -1
  14. package/dist/migrations/index.js.map +1 -1
  15. package/dist/migrations/manifests/0.3.6.json +9 -0
  16. package/dist/migrations/manifests/0.3.7.json +9 -0
  17. package/dist/templates/claude/commands/trellis/brainstorm.md +13 -0
  18. package/dist/templates/claude/commands/trellis/record-session.md +4 -1
  19. package/dist/templates/claude/commands/trellis/start.md +20 -4
  20. package/dist/templates/claude/hooks/inject-subagent-context.py +1 -1
  21. package/dist/templates/claude/hooks/session-start.py +86 -23
  22. package/dist/templates/claude/settings.json +10 -0
  23. package/dist/templates/codex/skills/brainstorm/SKILL.md +13 -0
  24. package/dist/templates/codex/skills/record-session/SKILL.md +4 -1
  25. package/dist/templates/codex/skills/start/SKILL.md +20 -4
  26. package/dist/templates/cursor/commands/trellis-brainstorm.md +13 -0
  27. package/dist/templates/cursor/commands/trellis-record-session.md +4 -1
  28. package/dist/templates/cursor/commands/trellis-start.md +20 -4
  29. package/dist/templates/gemini/commands/trellis/brainstorm.toml +15 -0
  30. package/dist/templates/gemini/commands/trellis/record-session.toml +4 -1
  31. package/dist/templates/gemini/commands/trellis/start.toml +60 -3
  32. package/dist/templates/iflow/commands/trellis/brainstorm.md +13 -0
  33. package/dist/templates/iflow/commands/trellis/record-session.md +4 -1
  34. package/dist/templates/iflow/commands/trellis/start.md +20 -4
  35. package/dist/templates/iflow/hooks/inject-subagent-context.py +1 -1
  36. package/dist/templates/iflow/hooks/session-start.py +86 -23
  37. package/dist/templates/kilo/workflows/brainstorm.md +13 -0
  38. package/dist/templates/kilo/workflows/record-session.md +4 -1
  39. package/dist/templates/kilo/workflows/start.md +64 -3
  40. package/dist/templates/kiro/skills/brainstorm/SKILL.md +13 -0
  41. package/dist/templates/kiro/skills/record-session/SKILL.md +4 -1
  42. package/dist/templates/kiro/skills/start/SKILL.md +20 -4
  43. package/dist/templates/markdown/spec/backend/directory-structure.md +292 -0
  44. package/dist/templates/markdown/spec/backend/script-conventions.md +220 -38
  45. package/dist/templates/opencode/commands/trellis/brainstorm.md +13 -0
  46. package/dist/templates/opencode/commands/trellis/record-session.md +4 -1
  47. package/dist/templates/opencode/commands/trellis/start.md +9 -1
  48. package/dist/templates/opencode/plugin/session-start.js +149 -16
  49. package/dist/templates/qoder/skills/brainstorm/SKILL.md +13 -0
  50. package/dist/templates/qoder/skills/record-session/SKILL.md +4 -1
  51. package/dist/templates/qoder/skills/start/SKILL.md +60 -3
  52. package/dist/templates/trellis/config.yaml +18 -0
  53. package/dist/templates/trellis/index.d.ts.map +1 -1
  54. package/dist/templates/trellis/index.js.map +1 -1
  55. package/dist/templates/trellis/scripts/common/config.py +20 -0
  56. package/dist/templates/trellis/scripts/common/git_context.py +160 -12
  57. package/dist/templates/trellis/scripts/common/task_queue.py +4 -0
  58. package/dist/templates/trellis/scripts/common/worktree.py +78 -11
  59. package/dist/templates/trellis/scripts/create_bootstrap.py +3 -0
  60. package/dist/templates/trellis/scripts/task.py +312 -17
  61. package/dist/utils/template-fetcher.d.ts +60 -7
  62. package/dist/utils/template-fetcher.d.ts.map +1 -1
  63. package/dist/utils/template-fetcher.js +183 -14
  64. package/dist/utils/template-fetcher.js.map +1 -1
  65. package/package.json +7 -9
@@ -50,6 +50,10 @@ cat .trellis/spec/backend/index.md # Backend guidelines
50
50
  cat .trellis/spec/guides/index.md # Thinking guides
51
51
  ```
52
52
 
53
+ > **Important**: The index files are navigation — they list the actual guideline files (e.g., `error-handling.md`, `conventions.md`, `mock-strategies.md`).
54
+ > At this step, just read the indexes to understand what's available.
55
+ > When you start actual development, you MUST go back and read the specific guideline files relevant to your task, as listed in the index's Pre-Development Checklist.
56
+
53
57
  ### Step 4: Report and Ask
54
58
 
55
59
  Report what you learned and ask: "What would you like to work on?"
@@ -63,12 +67,28 @@ When user describes a task, classify it:
63
67
  | Type | Criteria | Workflow |
64
68
  |------|----------|----------|
65
69
  | **Question** | User asks about code, architecture, or how something works | Answer directly |
66
- | **Trivial Fix** | Typo fix, comment update, single-line change, < 5 minutes | Direct Edit |
67
- | **Development Task** | Any code change that: modifies logic, adds features, fixes bugs, touches multiple files | **Task Workflow** |
70
+ | **Trivial Fix** | Typo fix, comment update, single-line change | Direct Edit |
71
+ | **Simple Task** | Clear goal, 1-2 files, well-defined scope | Quick confirm Implement |
72
+ | **Complex Task** | Vague goal, multiple files, architectural decisions | **Brainstorm → Task Workflow** |
73
+
74
+ ### Classification Signals
75
+
76
+ **Trivial/Simple indicators:**
77
+ - User specifies exact file and change
78
+ - "Fix the typo in X"
79
+ - "Add field Y to component Z"
80
+ - Clear acceptance criteria already stated
81
+
82
+ **Complex indicators:**
83
+ - "I want to add a feature for..."
84
+ - "Can you help me improve..."
85
+ - Mentions multiple areas or systems
86
+ - No clear implementation path
87
+ - User seems unsure about approach
68
88
 
69
89
  ### Decision Rule
70
90
 
71
- > **If in doubt, use Task Workflow.**
91
+ > **If in doubt, use Brainstorm + Task Workflow.**
72
92
  >
73
93
  > Task Workflow ensures specs are injected to agents, resulting in higher quality code.
74
94
  > The overhead is minimal, but the benefit is significant.
@@ -84,6 +104,43 @@ For questions or trivial fixes, work directly:
84
104
 
85
105
  ---
86
106
 
107
+ ## Simple Task
108
+
109
+ For simple, well-defined tasks:
110
+
111
+ 1. Quick confirm: "I understand you want to [goal]. Shall I proceed?"
112
+ 2. If no, clarify and confirm again
113
+ 3. **If yes: execute ALL steps below without stopping. Do NOT ask for additional confirmation between steps.**
114
+ - Create task directory (Phase 1 Path B, Step 2)
115
+ - Write PRD (Step 3)
116
+ - Research codebase (Phase 2, Step 5)
117
+ - Configure context (Step 6)
118
+ - Activate task (Step 7)
119
+ - Implement (Phase 3, Step 8)
120
+ - Check quality (Step 9)
121
+ - Complete (Step 10)
122
+
123
+ ---
124
+
125
+ ## Complex Task - Brainstorm First
126
+
127
+ For complex or vague tasks, **automatically start the brainstorm process** — do NOT skip directly to implementation.
128
+
129
+ See the `$brainstorm` skill for the full process. Summary:
130
+
131
+ 1. **Acknowledge and classify** - State your understanding
132
+ 2. **Create task directory** - Track evolving requirements in `prd.md`
133
+ 3. **Ask questions one at a time** - Update PRD after each answer
134
+ 4. **Propose approaches** - For architectural decisions
135
+ 5. **Confirm final requirements** - Get explicit approval
136
+ 6. **Proceed to Task Workflow** - With clear requirements in PRD
137
+
138
+ > **Subtask Decomposition**: If brainstorm reveals multiple independent work items,
139
+ > consider creating subtasks using `--parent` flag or `add-subtask` command.
140
+ > See the brainstorm skill's Step 8 for details.
141
+
142
+ ---
143
+
87
144
  ## Task Workflow (Development Tasks)
88
145
 
89
146
  **Why this workflow?**
@@ -13,3 +13,21 @@ session_commit_message: "chore: record journal"
13
13
 
14
14
  # Maximum lines per journal file before rotating to a new one
15
15
  max_journal_lines: 2000
16
+
17
+ #-------------------------------------------------------------------------------
18
+ # Task Lifecycle Hooks
19
+ #-------------------------------------------------------------------------------
20
+
21
+ # Shell commands to run after task lifecycle events.
22
+ # Each hook receives TASK_JSON_PATH environment variable pointing to task.json.
23
+ # Hook failures print a warning but do not block the main operation.
24
+ #
25
+ # hooks:
26
+ # after_create:
27
+ # - "echo 'Task created'"
28
+ # after_start:
29
+ # - "echo 'Task started'"
30
+ # after_finish:
31
+ # - "echo 'Task finished'"
32
+ # after_archive:
33
+ # - "echo 'Task archived'"
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/templates/trellis/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAcH,eAAO,MAAM,WAAW,QAAsC,CAAC;AAG/D,eAAO,MAAM,UAAU,QAA6C,CAAC;AACrE,eAAO,MAAM,WAAW,QAA0C,CAAC;AACnE,eAAO,MAAM,eAAe,QAA8C,CAAC;AAC3E,eAAO,MAAM,gBAAgB,QAAgD,CAAC;AAC9E,eAAO,MAAM,cAAc,QAA6C,CAAC;AACzE,eAAO,MAAM,eAAe,QAA+C,CAAC;AAC5E,eAAO,MAAM,eAAe,QAA+C,CAAC;AAC5E,eAAO,MAAM,WAAW,QAA0C,CAAC;AACnE,eAAO,MAAM,cAAc,QAA6C,CAAC;AACzE,eAAO,MAAM,gBAAgB,QAAgD,CAAC;AAC9E,eAAO,MAAM,YAAY,QAA2C,CAAC;AAGrE,eAAO,MAAM,cAAc,QAAkD,CAAC;AAC9E,eAAO,MAAM,eAAe,QAA+C,CAAC;AAC5E,eAAO,MAAM,iBAAiB,QAAiD,CAAC;AAChF,eAAO,MAAM,gBAAgB,QAAgD,CAAC;AAC9E,eAAO,MAAM,kBAAkB,QAAmD,CAAC;AACnF,eAAO,MAAM,cAAc,QAA8C,CAAC;AAG1E,eAAO,MAAM,kBAAkB,QAA2C,CAAC;AAC3E,eAAO,MAAM,mBAAmB,QAA4C,CAAC;AAC7E,eAAO,MAAM,UAAU,QAAkC,CAAC;AAC1D,eAAO,MAAM,gBAAgB,QAAyC,CAAC;AACvE,eAAO,MAAM,gBAAgB,QAAyC,CAAC;AACvE,eAAO,MAAM,qBAAqB,QAA8C,CAAC;AAGjF,eAAO,MAAM,kBAAkB,QAA8B,CAAC;AAC9D,eAAO,MAAM,kBAAkB,QAA8B,CAAC;AAC9D,eAAO,MAAM,oBAAoB,QAAgC,CAAC;AAClE,eAAO,MAAM,iBAAiB,QAAgC,CAAC;AAE/D;;GAEG;AACH,wBAAgB,aAAa,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAoCnD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/templates/trellis/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAcH,eAAO,MAAM,WAAW,QAAsC,CAAC;AAG/D,eAAO,MAAM,UAAU,QAA6C,CAAC;AACrE,eAAO,MAAM,WAAW,QAA0C,CAAC;AACnE,eAAO,MAAM,eAAe,QAA8C,CAAC;AAC3E,eAAO,MAAM,gBAAgB,QAAgD,CAAC;AAC9E,eAAO,MAAM,cAAc,QAA6C,CAAC;AACzE,eAAO,MAAM,eAAe,QAA+C,CAAC;AAC5E,eAAO,MAAM,eAAe,QAA+C,CAAC;AAC5E,eAAO,MAAM,WAAW,QAA0C,CAAC;AACnE,eAAO,MAAM,cAAc,QAA6C,CAAC;AACzE,eAAO,MAAM,gBAAgB,QAAgD,CAAC;AAC9E,eAAO,MAAM,YAAY,QAA2C,CAAC;AAGrE,eAAO,MAAM,cAAc,QAAkD,CAAC;AAC9E,eAAO,MAAM,eAAe,QAA+C,CAAC;AAC5E,eAAO,MAAM,iBAAiB,QAAiD,CAAC;AAChF,eAAO,MAAM,gBAAgB,QAAgD,CAAC;AAC9E,eAAO,MAAM,kBAAkB,QAE9B,CAAC;AACF,eAAO,MAAM,cAAc,QAA8C,CAAC;AAG1E,eAAO,MAAM,kBAAkB,QAA2C,CAAC;AAC3E,eAAO,MAAM,mBAAmB,QAA4C,CAAC;AAC7E,eAAO,MAAM,UAAU,QAAkC,CAAC;AAC1D,eAAO,MAAM,gBAAgB,QAAyC,CAAC;AACvE,eAAO,MAAM,gBAAgB,QAAyC,CAAC;AACvE,eAAO,MAAM,qBAAqB,QAEjC,CAAC;AAGF,eAAO,MAAM,kBAAkB,QAA8B,CAAC;AAC9D,eAAO,MAAM,kBAAkB,QAA8B,CAAC;AAC9D,eAAO,MAAM,oBAAoB,QAAgC,CAAC;AAClE,eAAO,MAAM,iBAAiB,QAAgC,CAAC;AAE/D;;GAEG;AACH,wBAAgB,aAAa,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAoCnD"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/templates/trellis/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/templates/trellis/index.ts"],"names":[],"mappings":"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"}
@@ -50,3 +50,23 @@ def get_max_journal_lines(repo_root: Path | None = None) -> int:
50
50
  return int(value)
51
51
  except (ValueError, TypeError):
52
52
  return DEFAULT_MAX_JOURNAL_LINES
53
+
54
+
55
+ def get_hooks(event: str, repo_root: Path | None = None) -> list[str]:
56
+ """Get hook commands for a lifecycle event.
57
+
58
+ Args:
59
+ event: Event name (e.g. "after_create", "after_archive").
60
+ repo_root: Repository root path.
61
+
62
+ Returns:
63
+ List of shell commands to execute, empty if none configured.
64
+ """
65
+ config = _load_config(repo_root)
66
+ hooks = config.get("hooks")
67
+ if not isinstance(hooks, dict):
68
+ return []
69
+ commands = hooks.get(event)
70
+ if isinstance(commands, list):
71
+ return [str(c) for c in commands]
72
+ return []
@@ -11,7 +11,6 @@ Provides:
11
11
  from __future__ import annotations
12
12
 
13
13
  import json
14
- import sys
15
14
  import subprocess
16
15
  from pathlib import Path
17
16
 
@@ -127,6 +126,8 @@ def get_context_json(repo_root: Path | None = None) -> dict:
127
126
  "dir": d.name,
128
127
  "name": data.get("name") or data.get("id") or "unknown",
129
128
  "status": data.get("status", "unknown"),
129
+ "children": data.get("children", []),
130
+ "parent": data.get("parent"),
130
131
  }
131
132
  )
132
133
 
@@ -263,6 +264,8 @@ def get_context_text(repo_root: Path | None = None) -> str:
263
264
  tasks_dir = get_tasks_dir(repo_root)
264
265
  task_count = 0
265
266
 
267
+ # Collect all task data for hierarchy display
268
+ all_task_data: dict[str, dict] = {}
266
269
  if tasks_dir.is_dir():
267
270
  for d in sorted(tasks_dir.iterdir()):
268
271
  if d.is_dir() and d.name != "archive":
@@ -270,15 +273,47 @@ def get_context_text(repo_root: Path | None = None) -> str:
270
273
  t_json = d / FILE_TASK_JSON
271
274
  status = "unknown"
272
275
  assignee = "-"
276
+ children: list[str] = []
277
+ parent: str | None = None
273
278
 
274
279
  if t_json.is_file():
275
280
  data = _read_json_file(t_json)
276
281
  if data:
277
282
  status = data.get("status", "unknown")
278
283
  assignee = data.get("assignee", "-")
279
-
280
- lines.append(f"- {dir_name}/ ({status}) @{assignee}")
281
- task_count += 1
284
+ children = data.get("children", [])
285
+ parent = data.get("parent")
286
+
287
+ all_task_data[dir_name] = {
288
+ "status": status,
289
+ "assignee": assignee,
290
+ "children": children,
291
+ "parent": parent,
292
+ }
293
+
294
+ def _children_progress(children_list: list[str]) -> str:
295
+ if not children_list:
296
+ return ""
297
+ done = 0
298
+ for c in children_list:
299
+ if c in all_task_data and all_task_data[c]["status"] in ("completed", "done"):
300
+ done += 1
301
+ return f" [{done}/{len(children_list)} done]"
302
+
303
+ def _print_task_tree(name: str, indent: int = 0) -> None:
304
+ nonlocal task_count
305
+ info = all_task_data[name]
306
+ progress = _children_progress(info["children"]) if info["children"] else ""
307
+ prefix = " " * indent
308
+ lines.append(f"{prefix}- {name}/ ({info['status']}){progress} @{info['assignee']}")
309
+ task_count += 1
310
+ for child in info["children"]:
311
+ if child in all_task_data:
312
+ _print_task_tree(child, indent + 1)
313
+
314
+ for dir_name in sorted(all_task_data.keys()):
315
+ if not all_task_data[dir_name]["parent"]:
316
+ _print_task_tree(dir_name)
282
317
 
283
318
  if task_count == 0:
284
319
  lines.append("(no active tasks)")
@@ -302,7 +337,9 @@ def get_context_text(repo_root: Path | None = None) -> str:
302
337
  if assignee == developer and status != "done":
303
338
  title = data.get("title") or data.get("name") or "unknown"
304
339
  priority = data.get("priority", "P2")
305
- lines.append(f"- [{priority}] {title} ({status})")
340
+ children_list = data.get("children", [])
341
+ progress = _children_progress(children_list) if children_list else ""
342
+ lines.append(f"- [{priority}] {title} ({status}){progress}")
306
343
  my_task_count += 1
307
344
 
308
345
  if my_task_count == 0:
@@ -335,6 +372,91 @@ def get_context_text(repo_root: Path | None = None) -> str:
335
372
  return "\n".join(lines)
336
373
 
337
374
 
375
+ def get_context_record_json(repo_root: Path | None = None) -> dict:
376
+ """Get record-mode context as a dictionary.
377
+
378
+ Focused on: my active tasks, git status, current task.
379
+ """
380
+ if repo_root is None:
381
+ repo_root = get_repo_root()
382
+
383
+ developer = get_developer(repo_root)
384
+ tasks_dir = get_tasks_dir(repo_root)
385
+
386
+ # Git info
387
+ _, branch_out, _ = _run_git_command(["branch", "--show-current"], cwd=repo_root)
388
+ branch = branch_out.strip() or "unknown"
389
+
390
+ _, status_out, _ = _run_git_command(["status", "--porcelain"], cwd=repo_root)
391
+ git_status_count = len([line for line in status_out.splitlines() if line.strip()])
392
+
393
+ _, log_out, _ = _run_git_command(["log", "--oneline", "-5"], cwd=repo_root)
394
+ commits = []
395
+ for line in log_out.splitlines():
396
+ if line.strip():
397
+ parts = line.split(" ", 1)
398
+ if len(parts) >= 2:
399
+ commits.append({"hash": parts[0], "message": parts[1]})
400
+
401
+ # My tasks
402
+ my_tasks = []
403
+ all_task_statuses: dict[str, str] = {}
404
+ if tasks_dir.is_dir():
405
+ for d in sorted(tasks_dir.iterdir()):
406
+ if d.is_dir() and d.name != "archive":
407
+ t_json = d / FILE_TASK_JSON
408
+ if t_json.is_file():
409
+ data = _read_json_file(t_json)
410
+ if data:
411
+ all_task_statuses[d.name] = data.get("status", "unknown")
412
+
413
+ if tasks_dir.is_dir():
414
+ for d in sorted(tasks_dir.iterdir()):
415
+ if d.is_dir() and d.name != "archive":
416
+ t_json = d / FILE_TASK_JSON
417
+ if t_json.is_file():
418
+ data = _read_json_file(t_json)
419
+ if data and data.get("assignee") == developer:
420
+ children_list = data.get("children", [])
421
+ done = sum(1 for c in children_list if all_task_statuses.get(c) in ("completed", "done"))
422
+ my_tasks.append({
423
+ "dir": d.name,
424
+ "title": data.get("title") or data.get("name") or "unknown",
425
+ "status": data.get("status", "unknown"),
426
+ "priority": data.get("priority", "P2"),
427
+ "children": children_list,
428
+ "childrenDone": done,
429
+ "parent": data.get("parent"),
430
+ "meta": data.get("meta", {}),
431
+ })
432
+
433
+ # Current task
434
+ current_task_info = None
435
+ current_task = get_current_task(repo_root)
436
+ if current_task:
437
+ task_json_path = (repo_root / current_task) / FILE_TASK_JSON
438
+ if task_json_path.is_file():
439
+ data = _read_json_file(task_json_path)
440
+ if data:
441
+ current_task_info = {
442
+ "path": current_task,
443
+ "name": data.get("name") or data.get("id") or "unknown",
444
+ "status": data.get("status", "unknown"),
445
+ }
446
+
447
+ return {
448
+ "developer": developer or "",
449
+ "git": {
450
+ "branch": branch,
451
+ "isClean": git_status_count == 0,
452
+ "uncommittedChanges": git_status_count,
453
+ "recentCommits": commits,
454
+ },
455
+ "myTasks": my_tasks,
456
+ "currentTask": current_task_info,
457
+ }
458
+
459
+
338
460
  def get_context_text_record(repo_root: Path | None = None) -> str:
339
461
  """Get context as formatted text for record-session mode.
340
462
 
@@ -371,6 +493,26 @@ def get_context_text_record(repo_root: Path | None = None) -> str:
371
493
  tasks_dir = get_tasks_dir(repo_root)
372
494
  my_task_count = 0
373
495
 
496
+ # Collect task data for children progress
497
+ all_task_statuses: dict[str, str] = {}
498
+ if tasks_dir.is_dir():
499
+ for d in sorted(tasks_dir.iterdir()):
500
+ if d.is_dir() and d.name != "archive":
501
+ t_json = d / FILE_TASK_JSON
502
+ if t_json.is_file():
503
+ data = _read_json_file(t_json)
504
+ if data:
505
+ all_task_statuses[d.name] = data.get("status", "unknown")
506
+
507
+ def _record_children_progress(children_list: list[str]) -> str:
508
+ if not children_list:
509
+ return ""
510
+ done = 0
511
+ for c in children_list:
512
+ if all_task_statuses.get(c) in ("completed", "done"):
513
+ done += 1
514
+ return f" [{done}/{len(children_list)} done]"
515
+
374
516
  if tasks_dir.is_dir():
375
517
  for d in sorted(tasks_dir.iterdir()):
376
518
  if d.is_dir() and d.name != "archive":
@@ -384,7 +526,9 @@ def get_context_text_record(repo_root: Path | None = None) -> str:
384
526
  if assignee == developer:
385
527
  title = data.get("title") or data.get("name") or "unknown"
386
528
  priority = data.get("priority", "P2")
387
- lines.append(f"- [{priority}] {title} ({status}) — {d.name}")
529
+ children_list = data.get("children", [])
530
+ progress = _record_children_progress(children_list) if children_list else ""
531
+ lines.append(f"- [{priority}] {title} ({status}){progress} — {d.name}")
388
532
  my_task_count += 1
389
533
 
390
534
  if my_task_count == 0:
@@ -469,7 +613,7 @@ def main() -> None:
469
613
  "--json",
470
614
  "-j",
471
615
  action="store_true",
472
- help="Output context in JSON format",
616
+ help="Output in JSON format (works with any --mode)",
473
617
  )
474
618
  parser.add_argument(
475
619
  "--mode",
@@ -481,12 +625,16 @@ def main() -> None:
481
625
 
482
626
  args = parser.parse_args()
483
627
 
484
- if args.json:
485
- output_json()
486
- elif args.mode == "record":
487
- print(get_context_text_record())
628
+ if args.mode == "record":
629
+ if args.json:
630
+ print(json.dumps(get_context_record_json(), indent=2, ensure_ascii=False))
631
+ else:
632
+ print(get_context_text_record())
488
633
  else:
489
- output_text()
634
+ if args.json:
635
+ output_json()
636
+ else:
637
+ output_text()
490
638
 
491
639
 
492
640
  if __name__ == "__main__":
@@ -86,6 +86,8 @@ def list_tasks_by_status(
86
86
  "status": status,
87
87
  "assignee": assignee,
88
88
  "dir": d.name,
89
+ "children": data.get("children", []),
90
+ "parent": data.get("parent"),
89
91
  })
90
92
 
91
93
  return results
@@ -161,6 +163,8 @@ def list_tasks_by_assignee(
161
163
  "status": status,
162
164
  "assignee": task_assignee,
163
165
  "dir": d.name,
166
+ "children": data.get("children", []),
167
+ "parent": data.get("parent"),
164
168
  })
165
169
 
166
170
  return results
@@ -26,39 +26,106 @@ from .paths import (
26
26
  # =============================================================================
27
27
 
28
28
  def parse_simple_yaml(content: str) -> dict:
29
- """Parse simple YAML (only supports key: value and lists).
29
+ """Parse simple YAML with nested dict support (no dependencies).
30
+
31
+ Supports:
32
+ - key: value (string)
33
+ - key: (followed by list items)
34
+ - item1
35
+ - item2
36
+ - key: (followed by nested dict)
37
+ nested_key: value
38
+ nested_key2:
39
+ - item
40
+
41
+ Uses indentation to detect nesting (2+ spaces deeper = child).
30
42
 
31
43
  Args:
32
44
  content: YAML content string.
33
45
 
34
46
  Returns:
35
- Parsed dict.
47
+ Parsed dict (values can be str, list[str], or dict).
36
48
  """
49
+ lines = content.splitlines()
37
50
  result: dict = {}
51
+ _parse_yaml_block(lines, 0, 0, result)
52
+ return result
53
+
54
+
55
+ def _parse_yaml_block(
56
+ lines: list[str], start: int, min_indent: int, target: dict
57
+ ) -> int:
58
+ """Parse a YAML block into target dict, returning next line index."""
59
+ i = start
38
60
  current_list: list | None = None
39
61
 
40
- for line in content.splitlines():
62
+ while i < len(lines):
63
+ line = lines[i]
41
64
  stripped = line.strip()
65
+
66
+ # Skip empty lines and comments
42
67
  if not stripped or stripped.startswith("#"):
68
+ i += 1
43
69
  continue
44
70
 
71
+ # Calculate indentation
72
+ indent = len(line) - len(line.lstrip())
73
+
74
+ # If dedented past our block, we're done
75
+ if indent < min_indent:
76
+ break
77
+
45
78
  if stripped.startswith("- "):
46
79
  if current_list is not None:
47
80
  current_list.append(stripped[2:].strip().strip('"').strip("'"))
81
+ i += 1
48
82
  elif ":" in stripped:
49
83
  key, _, value = stripped.partition(":")
50
84
  key = key.strip()
51
85
  value = value.strip().strip('"').strip("'")
86
+ current_list = None
87
+
52
88
  if value:
53
- result[key] = value
54
- _ = None
55
- current_list = None
89
+ # key: value
90
+ target[key] = value
91
+ i += 1
56
92
  else:
57
- _ = key
58
- current_list = []
59
- result[key] = current_list
60
-
61
- return result
93
+ # key: (no value) — peek ahead to determine list vs nested dict
94
+ next_i, next_line = _next_content_line(lines, i + 1)
95
+ if next_i >= len(lines):
96
+ target[key] = {}
97
+ i = next_i
98
+ elif next_line.strip().startswith("- "):
99
+ # It's a list
100
+ current_list = []
101
+ target[key] = current_list
102
+ i += 1
103
+ else:
104
+ next_indent = len(next_line) - len(next_line.lstrip())
105
+ if next_indent > indent:
106
+ # It's a nested dict
107
+ nested: dict = {}
108
+ target[key] = nested
109
+ i = _parse_yaml_block(lines, i + 1, next_indent, nested)
110
+ else:
111
+ # Empty value, same or less indent follows
112
+ target[key] = {}
113
+ i += 1
114
+ else:
115
+ i += 1
116
+
117
+ return i
118
+
119
+
120
+ def _next_content_line(lines: list[str], start: int) -> tuple[int, str]:
121
+ """Find the next non-empty, non-comment line."""
122
+ i = start
123
+ while i < len(lines):
124
+ stripped = lines[i].strip()
125
+ if stripped and not stripped.startswith("#"):
126
+ return i, lines[i]
127
+ i += 1
128
+ return i, ""
62
129
 
63
130
 
64
131
  def _yaml_get_value(config_file: Path, key: str) -> str | None:
@@ -228,8 +228,11 @@ def write_task_json(task_dir: Path, developer: str, project_type: str) -> None:
228
228
  "completedAt": None,
229
229
  "commit": None,
230
230
  "subtasks": subtasks,
231
+ "children": [],
232
+ "parent": None,
231
233
  "relatedFiles": related_files,
232
234
  "notes": f"First-time setup task created by trellis init ({project_type} project)",
235
+ "meta": {},
233
236
  }
234
237
 
235
238
  task_json = task_dir / "task.json"