@mindfoldhq/trellis 0.3.8 → 0.3.10-beta.0

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 (174) 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.3.9.json +9 -0
  19. package/dist/migrations/manifests/0.4.0-beta.1.json +228 -0
  20. package/dist/templates/claude/agents/dispatch.md +1 -2
  21. package/dist/templates/claude/agents/implement.md +2 -3
  22. package/dist/templates/claude/commands/trellis/before-dev.md +29 -0
  23. package/dist/templates/claude/commands/trellis/check.md +25 -0
  24. package/dist/templates/claude/commands/trellis/create-command.md +2 -2
  25. package/dist/templates/claude/commands/trellis/onboard.md +13 -13
  26. package/dist/templates/claude/commands/trellis/parallel.md +1 -2
  27. package/dist/templates/claude/commands/trellis/record-session.md +1 -1
  28. package/dist/templates/claude/commands/trellis/start.md +8 -4
  29. package/dist/templates/claude/hooks/inject-subagent-context.py +21 -13
  30. package/dist/templates/claude/hooks/session-start.py +170 -2
  31. package/dist/templates/codex/skills/before-dev/SKILL.md +34 -0
  32. package/dist/templates/codex/skills/check/SKILL.md +30 -0
  33. package/dist/templates/codex/skills/create-command/SKILL.md +2 -2
  34. package/dist/templates/codex/skills/onboard/SKILL.md +11 -11
  35. package/dist/templates/codex/skills/record-session/SKILL.md +1 -1
  36. package/dist/templates/codex/skills/start/SKILL.md +8 -3
  37. package/dist/templates/cursor/commands/trellis-before-dev.md +29 -0
  38. package/dist/templates/cursor/commands/trellis-check.md +25 -0
  39. package/dist/templates/cursor/commands/trellis-create-command.md +2 -2
  40. package/dist/templates/cursor/commands/trellis-onboard.md +13 -13
  41. package/dist/templates/cursor/commands/trellis-record-session.md +1 -1
  42. package/dist/templates/cursor/commands/trellis-start.md +7 -16
  43. package/dist/templates/gemini/commands/trellis/before-dev.toml +33 -0
  44. package/dist/templates/gemini/commands/trellis/check.toml +29 -0
  45. package/dist/templates/gemini/commands/trellis/create-command.toml +2 -2
  46. package/dist/templates/gemini/commands/trellis/onboard.toml +2 -2
  47. package/dist/templates/gemini/commands/trellis/record-session.toml +1 -1
  48. package/dist/templates/gemini/commands/trellis/start.toml +9 -4
  49. package/dist/templates/iflow/agents/dispatch.md +1 -2
  50. package/dist/templates/iflow/agents/implement.md +2 -3
  51. package/dist/templates/iflow/commands/trellis/before-dev.md +29 -0
  52. package/dist/templates/iflow/commands/trellis/check.md +25 -0
  53. package/dist/templates/iflow/commands/trellis/create-command.md +2 -2
  54. package/dist/templates/iflow/commands/trellis/onboard.md +13 -13
  55. package/dist/templates/iflow/commands/trellis/parallel.md +1 -2
  56. package/dist/templates/iflow/commands/trellis/record-session.md +1 -1
  57. package/dist/templates/iflow/commands/trellis/start.md +8 -4
  58. package/dist/templates/iflow/hooks/inject-subagent-context.py +21 -13
  59. package/dist/templates/iflow/hooks/session-start.py +156 -1
  60. package/dist/templates/iflow/settings.json +2 -2
  61. package/dist/templates/kilo/workflows/before-dev.md +29 -0
  62. package/dist/templates/kilo/workflows/check.md +25 -0
  63. package/dist/templates/kilo/workflows/create-command.md +2 -2
  64. package/dist/templates/kilo/workflows/onboard.md +13 -13
  65. package/dist/templates/kilo/workflows/parallel.md +1 -2
  66. package/dist/templates/kilo/workflows/record-session.md +1 -1
  67. package/dist/templates/kilo/workflows/start.md +8 -3
  68. package/dist/templates/kiro/skills/before-dev/SKILL.md +34 -0
  69. package/dist/templates/kiro/skills/check/SKILL.md +30 -0
  70. package/dist/templates/kiro/skills/create-command/SKILL.md +2 -2
  71. package/dist/templates/kiro/skills/onboard/SKILL.md +11 -11
  72. package/dist/templates/kiro/skills/record-session/SKILL.md +1 -1
  73. package/dist/templates/kiro/skills/start/SKILL.md +8 -3
  74. package/dist/templates/markdown/spec/backend/script-conventions.md +93 -0
  75. package/dist/templates/opencode/agents/dispatch.md +1 -2
  76. package/dist/templates/opencode/agents/implement.md +2 -2
  77. package/dist/templates/opencode/agents/research.md +1 -2
  78. package/dist/templates/opencode/commands/trellis/before-dev.md +29 -0
  79. package/dist/templates/opencode/commands/trellis/check.md +25 -0
  80. package/dist/templates/opencode/commands/trellis/create-command.md +2 -2
  81. package/dist/templates/opencode/commands/trellis/onboard.md +13 -13
  82. package/dist/templates/opencode/commands/trellis/parallel.md +1 -2
  83. package/dist/templates/opencode/commands/trellis/record-session.md +1 -1
  84. package/dist/templates/opencode/commands/trellis/start.md +8 -3
  85. package/dist/templates/opencode/plugin/inject-subagent-context.js +45 -18
  86. package/dist/templates/opencode/plugin/session-start.js +149 -1
  87. package/dist/templates/qoder/skills/before-dev/SKILL.md +34 -0
  88. package/dist/templates/qoder/skills/check/SKILL.md +30 -0
  89. package/dist/templates/qoder/skills/create-command/SKILL.md +2 -2
  90. package/dist/templates/qoder/skills/onboard/SKILL.md +13 -13
  91. package/dist/templates/qoder/skills/record-session/SKILL.md +1 -1
  92. package/dist/templates/qoder/skills/start/SKILL.md +8 -3
  93. package/dist/templates/trellis/config.yaml +20 -0
  94. package/dist/templates/trellis/index.d.ts +11 -0
  95. package/dist/templates/trellis/index.d.ts.map +1 -1
  96. package/dist/templates/trellis/index.js +22 -0
  97. package/dist/templates/trellis/index.js.map +1 -1
  98. package/dist/templates/trellis/scripts/add_session.py +52 -7
  99. package/dist/templates/trellis/scripts/common/cli_adapter.py +33 -45
  100. package/dist/templates/trellis/scripts/common/config.py +152 -0
  101. package/dist/templates/trellis/scripts/common/git.py +31 -0
  102. package/dist/templates/trellis/scripts/common/git_context.py +23 -586
  103. package/dist/templates/trellis/scripts/common/io.py +37 -0
  104. package/dist/templates/trellis/scripts/common/log.py +45 -0
  105. package/dist/templates/trellis/scripts/common/packages_context.py +233 -0
  106. package/dist/templates/trellis/scripts/common/paths.py +46 -0
  107. package/dist/templates/trellis/scripts/common/phase.py +50 -49
  108. package/dist/templates/trellis/scripts/common/registry.py +41 -72
  109. package/dist/templates/trellis/scripts/common/session_context.py +466 -0
  110. package/dist/templates/trellis/scripts/common/task_context.py +384 -0
  111. package/dist/templates/trellis/scripts/common/task_queue.py +27 -98
  112. package/dist/templates/trellis/scripts/common/task_store.py +534 -0
  113. package/dist/templates/trellis/scripts/common/task_utils.py +96 -6
  114. package/dist/templates/trellis/scripts/common/tasks.py +109 -0
  115. package/dist/templates/trellis/scripts/common/types.py +112 -0
  116. package/dist/templates/trellis/scripts/create_bootstrap.py +31 -26
  117. package/dist/templates/trellis/scripts/hooks/linear_sync.py +243 -0
  118. package/dist/templates/trellis/scripts/multi_agent/_bootstrap.py +17 -0
  119. package/dist/templates/trellis/scripts/multi_agent/cleanup.py +43 -48
  120. package/dist/templates/trellis/scripts/multi_agent/create_pr.py +336 -45
  121. package/dist/templates/trellis/scripts/multi_agent/plan.py +2 -26
  122. package/dist/templates/trellis/scripts/multi_agent/start.py +126 -57
  123. package/dist/templates/trellis/scripts/multi_agent/status.py +12 -753
  124. package/dist/templates/trellis/scripts/multi_agent/status_display.py +542 -0
  125. package/dist/templates/trellis/scripts/multi_agent/status_monitor.py +225 -0
  126. package/dist/templates/trellis/scripts/task.py +50 -975
  127. package/dist/templates/trellis/workflow.md +21 -34
  128. package/dist/types/migration.d.ts +3 -1
  129. package/dist/types/migration.d.ts.map +1 -1
  130. package/dist/utils/project-detector.d.ts +23 -0
  131. package/dist/utils/project-detector.d.ts.map +1 -1
  132. package/dist/utils/project-detector.js +364 -0
  133. package/dist/utils/project-detector.js.map +1 -1
  134. package/dist/utils/template-fetcher.d.ts +2 -2
  135. package/dist/utils/template-fetcher.d.ts.map +1 -1
  136. package/dist/utils/template-fetcher.js +5 -5
  137. package/dist/utils/template-fetcher.js.map +1 -1
  138. package/package.json +1 -1
  139. package/dist/templates/claude/commands/trellis/before-backend-dev.md +0 -13
  140. package/dist/templates/claude/commands/trellis/before-frontend-dev.md +0 -13
  141. package/dist/templates/claude/commands/trellis/check-backend.md +0 -13
  142. package/dist/templates/claude/commands/trellis/check-frontend.md +0 -13
  143. package/dist/templates/codex/skills/before-backend-dev/SKILL.md +0 -18
  144. package/dist/templates/codex/skills/before-frontend-dev/SKILL.md +0 -18
  145. package/dist/templates/codex/skills/check-backend/SKILL.md +0 -18
  146. package/dist/templates/codex/skills/check-frontend/SKILL.md +0 -18
  147. package/dist/templates/cursor/commands/trellis-before-backend-dev.md +0 -13
  148. package/dist/templates/cursor/commands/trellis-before-frontend-dev.md +0 -13
  149. package/dist/templates/cursor/commands/trellis-check-backend.md +0 -13
  150. package/dist/templates/cursor/commands/trellis-check-frontend.md +0 -13
  151. package/dist/templates/gemini/commands/trellis/before-backend-dev.toml +0 -17
  152. package/dist/templates/gemini/commands/trellis/before-frontend-dev.toml +0 -17
  153. package/dist/templates/gemini/commands/trellis/check-backend.toml +0 -17
  154. package/dist/templates/gemini/commands/trellis/check-frontend.toml +0 -17
  155. package/dist/templates/iflow/commands/trellis/before-backend-dev.md +0 -13
  156. package/dist/templates/iflow/commands/trellis/before-frontend-dev.md +0 -13
  157. package/dist/templates/iflow/commands/trellis/check-backend.md +0 -13
  158. package/dist/templates/iflow/commands/trellis/check-frontend.md +0 -13
  159. package/dist/templates/kilo/workflows/before-backend-dev.md +0 -13
  160. package/dist/templates/kilo/workflows/before-frontend-dev.md +0 -13
  161. package/dist/templates/kilo/workflows/check-backend.md +0 -13
  162. package/dist/templates/kilo/workflows/check-frontend.md +0 -13
  163. package/dist/templates/kiro/skills/before-backend-dev/SKILL.md +0 -18
  164. package/dist/templates/kiro/skills/before-frontend-dev/SKILL.md +0 -18
  165. package/dist/templates/kiro/skills/check-backend/SKILL.md +0 -18
  166. package/dist/templates/kiro/skills/check-frontend/SKILL.md +0 -18
  167. package/dist/templates/opencode/commands/trellis/before-backend-dev.md +0 -13
  168. package/dist/templates/opencode/commands/trellis/before-frontend-dev.md +0 -13
  169. package/dist/templates/opencode/commands/trellis/check-backend.md +0 -13
  170. package/dist/templates/opencode/commands/trellis/check-frontend.md +0 -13
  171. package/dist/templates/qoder/skills/before-backend-dev/SKILL.md +0 -18
  172. package/dist/templates/qoder/skills/before-frontend-dev/SKILL.md +0 -18
  173. package/dist/templates/qoder/skills/check-backend/SKILL.md +0 -18
  174. package/dist/templates/qoder/skills/check-frontend/SKILL.md +0 -18
@@ -3,6 +3,8 @@
3
3
  """
4
4
  Git and Session Context utilities.
5
5
 
6
+ Entry shim — delegates to session_context and packages_context.
7
+
6
8
  Provides:
7
9
  output_json - Output context in JSON format
8
10
  output_text - Output context in text format
@@ -11,599 +13,29 @@ Provides:
11
13
  from __future__ import annotations
12
14
 
13
15
  import json
14
- import subprocess
15
- from pathlib import Path
16
16
 
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,
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,
30
29
  )
31
30
 
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))
31
+ # Backward-compatible alias — external modules import this name
32
+ _run_git_command = run_git
600
33
 
601
34
 
602
35
  # =============================================================================
603
36
  # Main Entry
604
37
  # =============================================================================
605
38
 
606
-
607
39
  def main() -> None:
608
40
  """CLI entry point."""
609
41
  import argparse
@@ -618,9 +50,9 @@ def main() -> None:
618
50
  parser.add_argument(
619
51
  "--mode",
620
52
  "-m",
621
- choices=["default", "record"],
53
+ choices=["default", "record", "packages"],
622
54
  default="default",
623
- help="Output mode: default (full context) or record (for record-session)",
55
+ help="Output mode: default (full context), record (for record-session), packages (package info only)",
624
56
  )
625
57
 
626
58
  args = parser.parse_args()
@@ -630,6 +62,11 @@ def main() -> None:
630
62
  print(json.dumps(get_context_record_json(), indent=2, ensure_ascii=False))
631
63
  else:
632
64
  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())
633
70
  else:
634
71
  if args.json:
635
72
  output_json()
@@ -0,0 +1,37 @@
1
+ """
2
+ JSON file I/O utilities.
3
+
4
+ Provides read_json and write_json as the single source of truth
5
+ for JSON file operations across all Trellis scripts.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from pathlib import Path
12
+
13
+
14
+ def read_json(path: Path) -> dict | None:
15
+ """Read and parse a JSON file.
16
+
17
+ Returns None if the file doesn't exist, is invalid JSON, or can't be read.
18
+ """
19
+ try:
20
+ return json.loads(path.read_text(encoding="utf-8"))
21
+ except (FileNotFoundError, json.JSONDecodeError, OSError):
22
+ return None
23
+
24
+
25
+ def write_json(path: Path, data: dict) -> bool:
26
+ """Write dict to JSON file with pretty formatting.
27
+
28
+ Returns True on success, False on error.
29
+ """
30
+ try:
31
+ path.write_text(
32
+ json.dumps(data, indent=2, ensure_ascii=False),
33
+ encoding="utf-8",
34
+ )
35
+ return True
36
+ except (OSError, IOError):
37
+ return False