@mindfoldhq/trellis 0.3.10-beta.0 → 0.3.10

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 (173) hide show
  1. package/dist/cli/index.js +0 -2
  2. package/dist/cli/index.js.map +1 -1
  3. package/dist/commands/init.d.ts +0 -1
  4. package/dist/commands/init.d.ts.map +1 -1
  5. package/dist/commands/init.js +31 -203
  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 +6 -154
  9. package/dist/commands/update.js.map +1 -1
  10. package/dist/configurators/workflow.d.ts +2 -6
  11. package/dist/configurators/workflow.d.ts.map +1 -1
  12. package/dist/configurators/workflow.js +58 -88
  13. package/dist/configurators/workflow.js.map +1 -1
  14. package/dist/migrations/index.d.ts +0 -1
  15. package/dist/migrations/index.d.ts.map +1 -1
  16. package/dist/migrations/index.js +0 -2
  17. package/dist/migrations/index.js.map +1 -1
  18. package/dist/migrations/manifests/0.3.10.json +9 -0
  19. package/dist/templates/claude/agents/dispatch.md +2 -1
  20. package/dist/templates/claude/agents/implement.md +3 -2
  21. package/dist/templates/claude/commands/trellis/before-backend-dev.md +13 -0
  22. package/dist/templates/claude/commands/trellis/before-frontend-dev.md +13 -0
  23. package/dist/templates/claude/commands/trellis/check-backend.md +13 -0
  24. package/dist/templates/claude/commands/trellis/check-frontend.md +13 -0
  25. package/dist/templates/claude/commands/trellis/create-command.md +2 -2
  26. package/dist/templates/claude/commands/trellis/onboard.md +13 -13
  27. package/dist/templates/claude/commands/trellis/parallel.md +2 -1
  28. package/dist/templates/claude/commands/trellis/record-session.md +2 -2
  29. package/dist/templates/claude/commands/trellis/start.md +4 -8
  30. package/dist/templates/claude/hooks/inject-subagent-context.py +13 -21
  31. package/dist/templates/claude/hooks/session-start.py +2 -170
  32. package/dist/templates/codex/skills/before-backend-dev/SKILL.md +18 -0
  33. package/dist/templates/codex/skills/before-frontend-dev/SKILL.md +18 -0
  34. package/dist/templates/codex/skills/check-backend/SKILL.md +18 -0
  35. package/dist/templates/codex/skills/check-frontend/SKILL.md +18 -0
  36. package/dist/templates/codex/skills/create-command/SKILL.md +2 -2
  37. package/dist/templates/codex/skills/onboard/SKILL.md +11 -11
  38. package/dist/templates/codex/skills/record-session/SKILL.md +2 -2
  39. package/dist/templates/codex/skills/start/SKILL.md +3 -8
  40. package/dist/templates/cursor/commands/trellis-before-backend-dev.md +13 -0
  41. package/dist/templates/cursor/commands/trellis-before-frontend-dev.md +13 -0
  42. package/dist/templates/cursor/commands/trellis-check-backend.md +13 -0
  43. package/dist/templates/cursor/commands/trellis-check-frontend.md +13 -0
  44. package/dist/templates/cursor/commands/trellis-create-command.md +2 -2
  45. package/dist/templates/cursor/commands/trellis-onboard.md +13 -13
  46. package/dist/templates/cursor/commands/trellis-record-session.md +2 -2
  47. package/dist/templates/cursor/commands/trellis-start.md +16 -7
  48. package/dist/templates/gemini/commands/trellis/before-backend-dev.toml +17 -0
  49. package/dist/templates/gemini/commands/trellis/before-frontend-dev.toml +17 -0
  50. package/dist/templates/gemini/commands/trellis/check-backend.toml +17 -0
  51. package/dist/templates/gemini/commands/trellis/check-frontend.toml +17 -0
  52. package/dist/templates/gemini/commands/trellis/create-command.toml +2 -2
  53. package/dist/templates/gemini/commands/trellis/onboard.toml +2 -2
  54. package/dist/templates/gemini/commands/trellis/record-session.toml +2 -2
  55. package/dist/templates/gemini/commands/trellis/start.toml +4 -9
  56. package/dist/templates/iflow/agents/dispatch.md +2 -1
  57. package/dist/templates/iflow/agents/implement.md +3 -2
  58. package/dist/templates/iflow/commands/trellis/before-backend-dev.md +13 -0
  59. package/dist/templates/iflow/commands/trellis/before-frontend-dev.md +13 -0
  60. package/dist/templates/iflow/commands/trellis/check-backend.md +13 -0
  61. package/dist/templates/iflow/commands/trellis/check-frontend.md +13 -0
  62. package/dist/templates/iflow/commands/trellis/create-command.md +2 -2
  63. package/dist/templates/iflow/commands/trellis/onboard.md +13 -13
  64. package/dist/templates/iflow/commands/trellis/parallel.md +2 -1
  65. package/dist/templates/iflow/commands/trellis/record-session.md +2 -2
  66. package/dist/templates/iflow/commands/trellis/start.md +4 -8
  67. package/dist/templates/iflow/hooks/inject-subagent-context.py +13 -21
  68. package/dist/templates/iflow/hooks/session-start.py +1 -156
  69. package/dist/templates/kilo/workflows/before-backend-dev.md +13 -0
  70. package/dist/templates/kilo/workflows/before-frontend-dev.md +13 -0
  71. package/dist/templates/kilo/workflows/check-backend.md +13 -0
  72. package/dist/templates/kilo/workflows/check-frontend.md +13 -0
  73. package/dist/templates/kilo/workflows/create-command.md +2 -2
  74. package/dist/templates/kilo/workflows/onboard.md +13 -13
  75. package/dist/templates/kilo/workflows/parallel.md +2 -1
  76. package/dist/templates/kilo/workflows/record-session.md +2 -2
  77. package/dist/templates/kilo/workflows/start.md +3 -8
  78. package/dist/templates/kiro/skills/before-backend-dev/SKILL.md +18 -0
  79. package/dist/templates/kiro/skills/before-frontend-dev/SKILL.md +18 -0
  80. package/dist/templates/kiro/skills/check-backend/SKILL.md +18 -0
  81. package/dist/templates/kiro/skills/check-frontend/SKILL.md +18 -0
  82. package/dist/templates/kiro/skills/create-command/SKILL.md +2 -2
  83. package/dist/templates/kiro/skills/onboard/SKILL.md +11 -11
  84. package/dist/templates/kiro/skills/record-session/SKILL.md +2 -2
  85. package/dist/templates/kiro/skills/start/SKILL.md +3 -8
  86. package/dist/templates/markdown/spec/backend/script-conventions.md +0 -93
  87. package/dist/templates/opencode/agents/dispatch.md +2 -1
  88. package/dist/templates/opencode/agents/implement.md +2 -2
  89. package/dist/templates/opencode/agents/research.md +2 -1
  90. package/dist/templates/opencode/commands/trellis/before-backend-dev.md +13 -0
  91. package/dist/templates/opencode/commands/trellis/before-frontend-dev.md +13 -0
  92. package/dist/templates/opencode/commands/trellis/check-backend.md +13 -0
  93. package/dist/templates/opencode/commands/trellis/check-frontend.md +13 -0
  94. package/dist/templates/opencode/commands/trellis/create-command.md +2 -2
  95. package/dist/templates/opencode/commands/trellis/onboard.md +13 -13
  96. package/dist/templates/opencode/commands/trellis/parallel.md +2 -1
  97. package/dist/templates/opencode/commands/trellis/record-session.md +2 -2
  98. package/dist/templates/opencode/commands/trellis/start.md +3 -8
  99. package/dist/templates/opencode/plugin/inject-subagent-context.js +18 -45
  100. package/dist/templates/opencode/plugin/session-start.js +1 -149
  101. package/dist/templates/qoder/skills/before-backend-dev/SKILL.md +18 -0
  102. package/dist/templates/qoder/skills/before-frontend-dev/SKILL.md +18 -0
  103. package/dist/templates/qoder/skills/check-backend/SKILL.md +18 -0
  104. package/dist/templates/qoder/skills/check-frontend/SKILL.md +18 -0
  105. package/dist/templates/qoder/skills/create-command/SKILL.md +2 -2
  106. package/dist/templates/qoder/skills/onboard/SKILL.md +13 -13
  107. package/dist/templates/qoder/skills/record-session/SKILL.md +2 -2
  108. package/dist/templates/qoder/skills/start/SKILL.md +3 -8
  109. package/dist/templates/trellis/config.yaml +0 -20
  110. package/dist/templates/trellis/index.d.ts +0 -11
  111. package/dist/templates/trellis/index.d.ts.map +1 -1
  112. package/dist/templates/trellis/index.js +0 -22
  113. package/dist/templates/trellis/index.js.map +1 -1
  114. package/dist/templates/trellis/scripts/add_session.py +7 -52
  115. package/dist/templates/trellis/scripts/common/cli_adapter.py +45 -33
  116. package/dist/templates/trellis/scripts/common/config.py +0 -152
  117. package/dist/templates/trellis/scripts/common/git_context.py +586 -23
  118. package/dist/templates/trellis/scripts/common/paths.py +0 -46
  119. package/dist/templates/trellis/scripts/common/phase.py +49 -50
  120. package/dist/templates/trellis/scripts/common/registry.py +72 -41
  121. package/dist/templates/trellis/scripts/common/task_queue.py +98 -27
  122. package/dist/templates/trellis/scripts/common/task_utils.py +6 -96
  123. package/dist/templates/trellis/scripts/create_bootstrap.py +26 -31
  124. package/dist/templates/trellis/scripts/multi_agent/cleanup.py +48 -43
  125. package/dist/templates/trellis/scripts/multi_agent/create_pr.py +45 -336
  126. package/dist/templates/trellis/scripts/multi_agent/plan.py +26 -2
  127. package/dist/templates/trellis/scripts/multi_agent/start.py +57 -126
  128. package/dist/templates/trellis/scripts/multi_agent/status.py +753 -12
  129. package/dist/templates/trellis/scripts/task.py +975 -50
  130. package/dist/templates/trellis/workflow.md +34 -21
  131. package/dist/types/migration.d.ts +1 -3
  132. package/dist/types/migration.d.ts.map +1 -1
  133. package/dist/utils/project-detector.d.ts +0 -23
  134. package/dist/utils/project-detector.d.ts.map +1 -1
  135. package/dist/utils/project-detector.js +0 -364
  136. package/dist/utils/project-detector.js.map +1 -1
  137. package/dist/utils/template-fetcher.d.ts +10 -2
  138. package/dist/utils/template-fetcher.d.ts.map +1 -1
  139. package/dist/utils/template-fetcher.js +43 -12
  140. package/dist/utils/template-fetcher.js.map +1 -1
  141. package/package.json +1 -1
  142. package/dist/migrations/manifests/0.4.0-beta.1.json +0 -228
  143. package/dist/templates/claude/commands/trellis/before-dev.md +0 -29
  144. package/dist/templates/claude/commands/trellis/check.md +0 -25
  145. package/dist/templates/codex/skills/before-dev/SKILL.md +0 -34
  146. package/dist/templates/codex/skills/check/SKILL.md +0 -30
  147. package/dist/templates/cursor/commands/trellis-before-dev.md +0 -29
  148. package/dist/templates/cursor/commands/trellis-check.md +0 -25
  149. package/dist/templates/gemini/commands/trellis/before-dev.toml +0 -33
  150. package/dist/templates/gemini/commands/trellis/check.toml +0 -29
  151. package/dist/templates/iflow/commands/trellis/before-dev.md +0 -29
  152. package/dist/templates/iflow/commands/trellis/check.md +0 -25
  153. package/dist/templates/kilo/workflows/before-dev.md +0 -29
  154. package/dist/templates/kilo/workflows/check.md +0 -25
  155. package/dist/templates/kiro/skills/before-dev/SKILL.md +0 -34
  156. package/dist/templates/kiro/skills/check/SKILL.md +0 -30
  157. package/dist/templates/opencode/commands/trellis/before-dev.md +0 -29
  158. package/dist/templates/opencode/commands/trellis/check.md +0 -25
  159. package/dist/templates/qoder/skills/before-dev/SKILL.md +0 -34
  160. package/dist/templates/qoder/skills/check/SKILL.md +0 -30
  161. package/dist/templates/trellis/scripts/common/git.py +0 -31
  162. package/dist/templates/trellis/scripts/common/io.py +0 -37
  163. package/dist/templates/trellis/scripts/common/log.py +0 -45
  164. package/dist/templates/trellis/scripts/common/packages_context.py +0 -233
  165. package/dist/templates/trellis/scripts/common/session_context.py +0 -466
  166. package/dist/templates/trellis/scripts/common/task_context.py +0 -384
  167. package/dist/templates/trellis/scripts/common/task_store.py +0 -534
  168. package/dist/templates/trellis/scripts/common/tasks.py +0 -109
  169. package/dist/templates/trellis/scripts/common/types.py +0 -112
  170. package/dist/templates/trellis/scripts/hooks/linear_sync.py +0 -243
  171. package/dist/templates/trellis/scripts/multi_agent/_bootstrap.py +0 -17
  172. package/dist/templates/trellis/scripts/multi_agent/status_display.py +0 -542
  173. package/dist/templates/trellis/scripts/multi_agent/status_monitor.py +0 -225
@@ -3,8 +3,6 @@
3
3
  """
4
4
  Git and Session Context utilities.
5
5
 
6
- Entry shim — delegates to session_context and packages_context.
7
-
8
6
  Provides:
9
7
  output_json - Output context in JSON format
10
8
  output_text - Output context in text format
@@ -13,29 +11,599 @@ Provides:
13
11
  from __future__ import annotations
14
12
 
15
13
  import json
14
+ import subprocess
15
+ from pathlib import Path
16
16
 
17
- from .git import run_git
18
- from .session_context import (
19
- get_context_json,
20
- get_context_text,
21
- get_context_record_json,
22
- get_context_text_record,
23
- output_json,
24
- output_text,
25
- )
26
- from .packages_context import (
27
- get_context_packages_text,
28
- get_context_packages_json,
17
+ from .paths import (
18
+ DIR_SCRIPTS,
19
+ DIR_SPEC,
20
+ DIR_TASKS,
21
+ DIR_WORKFLOW,
22
+ DIR_WORKSPACE,
23
+ FILE_TASK_JSON,
24
+ count_lines,
25
+ get_active_journal_file,
26
+ get_current_task,
27
+ get_developer,
28
+ get_repo_root,
29
+ get_tasks_dir,
29
30
  )
30
31
 
31
- # Backward-compatible alias — external modules import this name
32
- _run_git_command = run_git
32
+ # =============================================================================
33
+ # Helper Functions
34
+ # =============================================================================
35
+
36
+
37
+ def _run_git_command(args: list[str], cwd: Path | None = None) -> tuple[int, str, str]:
38
+ """Run a git command and return (returncode, stdout, stderr).
39
+
40
+ Uses UTF-8 encoding with -c i18n.logOutputEncoding=UTF-8 to ensure
41
+ consistent output across all platforms (Windows, macOS, Linux).
42
+ """
43
+ try:
44
+ # Force git to output UTF-8 for consistent cross-platform behavior
45
+ git_args = ["git", "-c", "i18n.logOutputEncoding=UTF-8"] + args
46
+ result = subprocess.run(
47
+ git_args,
48
+ cwd=cwd,
49
+ capture_output=True,
50
+ text=True,
51
+ encoding="utf-8",
52
+ errors="replace",
53
+ )
54
+ return result.returncode, result.stdout, result.stderr
55
+ except Exception as e:
56
+ return 1, "", str(e)
57
+
58
+
59
+ def _read_json_file(path: Path) -> dict | None:
60
+ """Read and parse a JSON file."""
61
+ try:
62
+ return json.loads(path.read_text(encoding="utf-8"))
63
+ except (FileNotFoundError, json.JSONDecodeError, OSError):
64
+ return None
65
+
66
+
67
+ # =============================================================================
68
+ # JSON Output
69
+ # =============================================================================
70
+
71
+
72
+ def get_context_json(repo_root: Path | None = None) -> dict:
73
+ """Get context as a dictionary.
74
+
75
+ Args:
76
+ repo_root: Repository root path. Defaults to auto-detected.
77
+
78
+ Returns:
79
+ Context dictionary.
80
+ """
81
+ if repo_root is None:
82
+ repo_root = get_repo_root()
83
+
84
+ developer = get_developer(repo_root)
85
+ tasks_dir = get_tasks_dir(repo_root)
86
+ journal_file = get_active_journal_file(repo_root)
87
+
88
+ journal_lines = 0
89
+ journal_relative = ""
90
+ if journal_file and developer:
91
+ journal_lines = count_lines(journal_file)
92
+ journal_relative = (
93
+ f"{DIR_WORKFLOW}/{DIR_WORKSPACE}/{developer}/{journal_file.name}"
94
+ )
95
+
96
+ # Git info
97
+ _, branch_out, _ = _run_git_command(["branch", "--show-current"], cwd=repo_root)
98
+ branch = branch_out.strip() or "unknown"
99
+
100
+ _, status_out, _ = _run_git_command(["status", "--porcelain"], cwd=repo_root)
101
+ git_status_count = len([line for line in status_out.splitlines() if line.strip()])
102
+ is_clean = git_status_count == 0
103
+
104
+ # Recent commits
105
+ _, log_out, _ = _run_git_command(["log", "--oneline", "-5"], cwd=repo_root)
106
+ commits = []
107
+ for line in log_out.splitlines():
108
+ if line.strip():
109
+ parts = line.split(" ", 1)
110
+ if len(parts) >= 2:
111
+ commits.append({"hash": parts[0], "message": parts[1]})
112
+ elif len(parts) == 1:
113
+ commits.append({"hash": parts[0], "message": ""})
114
+
115
+ # Tasks
116
+ tasks = []
117
+ if tasks_dir.is_dir():
118
+ for d in tasks_dir.iterdir():
119
+ if d.is_dir() and d.name != "archive":
120
+ task_json_path = d / FILE_TASK_JSON
121
+ if task_json_path.is_file():
122
+ data = _read_json_file(task_json_path)
123
+ if data:
124
+ tasks.append(
125
+ {
126
+ "dir": d.name,
127
+ "name": data.get("name") or data.get("id") or "unknown",
128
+ "status": data.get("status", "unknown"),
129
+ "children": data.get("children", []),
130
+ "parent": data.get("parent"),
131
+ }
132
+ )
133
+
134
+ return {
135
+ "developer": developer or "",
136
+ "git": {
137
+ "branch": branch,
138
+ "isClean": is_clean,
139
+ "uncommittedChanges": git_status_count,
140
+ "recentCommits": commits,
141
+ },
142
+ "tasks": {
143
+ "active": tasks,
144
+ "directory": f"{DIR_WORKFLOW}/{DIR_TASKS}",
145
+ },
146
+ "journal": {
147
+ "file": journal_relative,
148
+ "lines": journal_lines,
149
+ "nearLimit": journal_lines > 1800,
150
+ },
151
+ }
152
+
153
+
154
+ def output_json(repo_root: Path | None = None) -> None:
155
+ """Output context in JSON format.
156
+
157
+ Args:
158
+ repo_root: Repository root path. Defaults to auto-detected.
159
+ """
160
+ context = get_context_json(repo_root)
161
+ print(json.dumps(context, indent=2, ensure_ascii=False))
162
+
163
+
164
+ # =============================================================================
165
+ # Text Output
166
+ # =============================================================================
167
+
168
+
169
+ def get_context_text(repo_root: Path | None = None) -> str:
170
+ """Get context as formatted text.
171
+
172
+ Args:
173
+ repo_root: Repository root path. Defaults to auto-detected.
174
+
175
+ Returns:
176
+ Formatted text output.
177
+ """
178
+ if repo_root is None:
179
+ repo_root = get_repo_root()
180
+
181
+ lines = []
182
+ lines.append("========================================")
183
+ lines.append("SESSION CONTEXT")
184
+ lines.append("========================================")
185
+ lines.append("")
186
+
187
+ developer = get_developer(repo_root)
188
+
189
+ # Developer section
190
+ lines.append("## DEVELOPER")
191
+ if not developer:
192
+ lines.append(
193
+ f"ERROR: Not initialized. Run: python3 ./{DIR_WORKFLOW}/{DIR_SCRIPTS}/init_developer.py <name>"
194
+ )
195
+ return "\n".join(lines)
196
+
197
+ lines.append(f"Name: {developer}")
198
+ lines.append("")
199
+
200
+ # Git status
201
+ lines.append("## GIT STATUS")
202
+ _, branch_out, _ = _run_git_command(["branch", "--show-current"], cwd=repo_root)
203
+ branch = branch_out.strip() or "unknown"
204
+ lines.append(f"Branch: {branch}")
205
+
206
+ _, status_out, _ = _run_git_command(["status", "--porcelain"], cwd=repo_root)
207
+ status_lines = [line for line in status_out.splitlines() if line.strip()]
208
+ status_count = len(status_lines)
209
+
210
+ if status_count == 0:
211
+ lines.append("Working directory: Clean")
212
+ else:
213
+ lines.append(f"Working directory: {status_count} uncommitted change(s)")
214
+ lines.append("")
215
+ lines.append("Changes:")
216
+ _, short_out, _ = _run_git_command(["status", "--short"], cwd=repo_root)
217
+ for line in short_out.splitlines()[:10]:
218
+ lines.append(line)
219
+ lines.append("")
220
+
221
+ # Recent commits
222
+ lines.append("## RECENT COMMITS")
223
+ _, log_out, _ = _run_git_command(["log", "--oneline", "-5"], cwd=repo_root)
224
+ if log_out.strip():
225
+ for line in log_out.splitlines():
226
+ lines.append(line)
227
+ else:
228
+ lines.append("(no commits)")
229
+ lines.append("")
230
+
231
+ # Current task
232
+ lines.append("## CURRENT TASK")
233
+ current_task = get_current_task(repo_root)
234
+ if current_task:
235
+ current_task_dir = repo_root / current_task
236
+ task_json_path = current_task_dir / FILE_TASK_JSON
237
+ lines.append(f"Path: {current_task}")
238
+
239
+ if task_json_path.is_file():
240
+ data = _read_json_file(task_json_path)
241
+ if data:
242
+ t_name = data.get("name") or data.get("id") or "unknown"
243
+ t_status = data.get("status", "unknown")
244
+ t_created = data.get("createdAt", "unknown")
245
+ t_desc = data.get("description", "")
246
+
247
+ lines.append(f"Name: {t_name}")
248
+ lines.append(f"Status: {t_status}")
249
+ lines.append(f"Created: {t_created}")
250
+ if t_desc:
251
+ lines.append(f"Description: {t_desc}")
252
+
253
+ # Check for prd.md
254
+ prd_file = current_task_dir / "prd.md"
255
+ if prd_file.is_file():
256
+ lines.append("")
257
+ lines.append("[!] This task has prd.md - read it for task details")
258
+ else:
259
+ lines.append("(none)")
260
+ lines.append("")
261
+
262
+ # Active tasks
263
+ lines.append("## ACTIVE TASKS")
264
+ tasks_dir = get_tasks_dir(repo_root)
265
+ task_count = 0
266
+
267
+ # Collect all task data for hierarchy display
268
+ all_task_data: dict[str, dict] = {}
269
+ if tasks_dir.is_dir():
270
+ for d in sorted(tasks_dir.iterdir()):
271
+ if d.is_dir() and d.name != "archive":
272
+ dir_name = d.name
273
+ t_json = d / FILE_TASK_JSON
274
+ status = "unknown"
275
+ assignee = "-"
276
+ children: list[str] = []
277
+ parent: str | None = None
278
+
279
+ if t_json.is_file():
280
+ data = _read_json_file(t_json)
281
+ if data:
282
+ status = data.get("status", "unknown")
283
+ assignee = data.get("assignee", "-")
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)
317
+
318
+ if task_count == 0:
319
+ lines.append("(no active tasks)")
320
+ lines.append(f"Total: {task_count} active task(s)")
321
+ lines.append("")
322
+
323
+ # My tasks
324
+ lines.append("## MY TASKS (Assigned to me)")
325
+ my_task_count = 0
326
+
327
+ if tasks_dir.is_dir():
328
+ for d in sorted(tasks_dir.iterdir()):
329
+ if d.is_dir() and d.name != "archive":
330
+ t_json = d / FILE_TASK_JSON
331
+ if t_json.is_file():
332
+ data = _read_json_file(t_json)
333
+ if data:
334
+ assignee = data.get("assignee", "")
335
+ status = data.get("status", "planning")
336
+
337
+ if assignee == developer and status != "done":
338
+ title = data.get("title") or data.get("name") or "unknown"
339
+ priority = data.get("priority", "P2")
340
+ children_list = data.get("children", [])
341
+ progress = _children_progress(children_list) if children_list else ""
342
+ lines.append(f"- [{priority}] {title} ({status}){progress}")
343
+ my_task_count += 1
344
+
345
+ if my_task_count == 0:
346
+ lines.append("(no tasks assigned to you)")
347
+ lines.append("")
348
+
349
+ # Journal file
350
+ lines.append("## JOURNAL FILE")
351
+ journal_file = get_active_journal_file(repo_root)
352
+ if journal_file:
353
+ journal_lines = count_lines(journal_file)
354
+ relative = f"{DIR_WORKFLOW}/{DIR_WORKSPACE}/{developer}/{journal_file.name}"
355
+ lines.append(f"Active file: {relative}")
356
+ lines.append(f"Line count: {journal_lines} / 2000")
357
+ if journal_lines > 1800:
358
+ lines.append("[!] WARNING: Approaching 2000 line limit!")
359
+ else:
360
+ lines.append("No journal file found")
361
+ lines.append("")
362
+
363
+ # Paths
364
+ lines.append("## PATHS")
365
+ lines.append(f"Workspace: {DIR_WORKFLOW}/{DIR_WORKSPACE}/{developer}/")
366
+ lines.append(f"Tasks: {DIR_WORKFLOW}/{DIR_TASKS}/")
367
+ lines.append(f"Spec: {DIR_WORKFLOW}/{DIR_SPEC}/")
368
+ lines.append("")
369
+
370
+ lines.append("========================================")
371
+
372
+ return "\n".join(lines)
373
+
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
+
460
+ def get_context_text_record(repo_root: Path | None = None) -> str:
461
+ """Get context as formatted text for record-session mode.
462
+
463
+ Focused output: MY ACTIVE TASKS first (with [!!!] emphasis),
464
+ then GIT STATUS, RECENT COMMITS, CURRENT TASK.
465
+
466
+ Args:
467
+ repo_root: Repository root path. Defaults to auto-detected.
468
+
469
+ Returns:
470
+ Formatted text output for record-session.
471
+ """
472
+ if repo_root is None:
473
+ repo_root = get_repo_root()
474
+
475
+ lines: list[str] = []
476
+ lines.append("========================================")
477
+ lines.append("SESSION CONTEXT (RECORD MODE)")
478
+ lines.append("========================================")
479
+ lines.append("")
480
+
481
+ developer = get_developer(repo_root)
482
+ if not developer:
483
+ lines.append(
484
+ f"ERROR: Not initialized. Run: python3 ./{DIR_WORKFLOW}/{DIR_SCRIPTS}/init_developer.py <name>"
485
+ )
486
+ return "\n".join(lines)
487
+
488
+ # MY ACTIVE TASKS — first and prominent
489
+ lines.append(f"## [!!!] MY ACTIVE TASKS (Assigned to {developer})")
490
+ lines.append("[!] Review whether any should be archived before recording this session.")
491
+ lines.append("")
492
+
493
+ tasks_dir = get_tasks_dir(repo_root)
494
+ my_task_count = 0
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
+
516
+ if tasks_dir.is_dir():
517
+ for d in sorted(tasks_dir.iterdir()):
518
+ if d.is_dir() and d.name != "archive":
519
+ t_json = d / FILE_TASK_JSON
520
+ if t_json.is_file():
521
+ data = _read_json_file(t_json)
522
+ if data:
523
+ assignee = data.get("assignee", "")
524
+ status = data.get("status", "planning")
525
+
526
+ if assignee == developer:
527
+ title = data.get("title") or data.get("name") or "unknown"
528
+ priority = data.get("priority", "P2")
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}")
532
+ my_task_count += 1
533
+
534
+ if my_task_count == 0:
535
+ lines.append("(no active tasks assigned to you)")
536
+ lines.append("")
537
+
538
+ # GIT STATUS
539
+ lines.append("## GIT STATUS")
540
+ _, branch_out, _ = _run_git_command(["branch", "--show-current"], cwd=repo_root)
541
+ branch = branch_out.strip() or "unknown"
542
+ lines.append(f"Branch: {branch}")
543
+
544
+ _, status_out, _ = _run_git_command(["status", "--porcelain"], cwd=repo_root)
545
+ status_lines = [line for line in status_out.splitlines() if line.strip()]
546
+ status_count = len(status_lines)
547
+
548
+ if status_count == 0:
549
+ lines.append("Working directory: Clean")
550
+ else:
551
+ lines.append(f"Working directory: {status_count} uncommitted change(s)")
552
+ lines.append("")
553
+ lines.append("Changes:")
554
+ _, short_out, _ = _run_git_command(["status", "--short"], cwd=repo_root)
555
+ for line in short_out.splitlines()[:10]:
556
+ lines.append(line)
557
+ lines.append("")
558
+
559
+ # RECENT COMMITS
560
+ lines.append("## RECENT COMMITS")
561
+ _, log_out, _ = _run_git_command(["log", "--oneline", "-5"], cwd=repo_root)
562
+ if log_out.strip():
563
+ for line in log_out.splitlines():
564
+ lines.append(line)
565
+ else:
566
+ lines.append("(no commits)")
567
+ lines.append("")
568
+
569
+ # CURRENT TASK
570
+ lines.append("## CURRENT TASK")
571
+ current_task = get_current_task(repo_root)
572
+ if current_task:
573
+ current_task_dir = repo_root / current_task
574
+ task_json_path = current_task_dir / FILE_TASK_JSON
575
+ lines.append(f"Path: {current_task}")
576
+
577
+ if task_json_path.is_file():
578
+ data = _read_json_file(task_json_path)
579
+ if data:
580
+ t_name = data.get("name") or data.get("id") or "unknown"
581
+ t_status = data.get("status", "unknown")
582
+ lines.append(f"Name: {t_name}")
583
+ lines.append(f"Status: {t_status}")
584
+ else:
585
+ lines.append("(none)")
586
+ lines.append("")
587
+
588
+ lines.append("========================================")
589
+
590
+ return "\n".join(lines)
591
+
592
+
593
+ def output_text(repo_root: Path | None = None) -> None:
594
+ """Output context in text format.
595
+
596
+ Args:
597
+ repo_root: Repository root path. Defaults to auto-detected.
598
+ """
599
+ print(get_context_text(repo_root))
33
600
 
34
601
 
35
602
  # =============================================================================
36
603
  # Main Entry
37
604
  # =============================================================================
38
605
 
606
+
39
607
  def main() -> None:
40
608
  """CLI entry point."""
41
609
  import argparse
@@ -50,9 +618,9 @@ def main() -> None:
50
618
  parser.add_argument(
51
619
  "--mode",
52
620
  "-m",
53
- choices=["default", "record", "packages"],
621
+ choices=["default", "record"],
54
622
  default="default",
55
- help="Output mode: default (full context), record (for record-session), packages (package info only)",
623
+ help="Output mode: default (full context) or record (for record-session)",
56
624
  )
57
625
 
58
626
  args = parser.parse_args()
@@ -62,11 +630,6 @@ def main() -> None:
62
630
  print(json.dumps(get_context_record_json(), indent=2, ensure_ascii=False))
63
631
  else:
64
632
  print(get_context_text_record())
65
- elif args.mode == "packages":
66
- if args.json:
67
- print(json.dumps(get_context_packages_json(), indent=2, ensure_ascii=False))
68
- else:
69
- print(get_context_packages_text())
70
633
  else:
71
634
  if args.json:
72
635
  output_json()
@@ -333,52 +333,6 @@ def generate_task_date_prefix() -> str:
333
333
  return datetime.now().strftime("%m-%d")
334
334
 
335
335
 
336
- # =============================================================================
337
- # Monorepo / Package Paths
338
- # =============================================================================
339
-
340
-
341
- def get_spec_dir(package: str | None = None, repo_root: Path | None = None) -> Path:
342
- """Get the spec directory path.
343
-
344
- Single-repo: .trellis/spec
345
- Monorepo with package: .trellis/spec/<package>
346
-
347
- Uses lazy import to avoid circular dependency with config.py.
348
- """
349
- if repo_root is None:
350
- repo_root = get_repo_root()
351
-
352
- from .config import get_spec_base
353
-
354
- base = get_spec_base(package, repo_root)
355
- return repo_root / DIR_WORKFLOW / base
356
-
357
-
358
- def get_package_path(package: str, repo_root: Path | None = None) -> Path | None:
359
- """Get a package's source directory absolute path from config.
360
-
361
- Returns:
362
- Absolute path to the package directory, or None if not found.
363
- """
364
- if repo_root is None:
365
- repo_root = get_repo_root()
366
-
367
- from .config import get_packages
368
-
369
- packages = get_packages(repo_root)
370
- if not packages or package not in packages:
371
- return None
372
-
373
- info = packages[package]
374
- if isinstance(info, dict):
375
- rel_path = info.get("path", package)
376
- else:
377
- rel_path = str(info)
378
-
379
- return repo_root / rel_path
380
-
381
-
382
336
  # =============================================================================
383
337
  # Main Entry (for testing)
384
338
  # =============================================================================