@mindfoldhq/trellis 0.4.0-beta.1 → 0.4.0-beta.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 (200) hide show
  1. package/README.md +19 -5
  2. package/dist/cli/index.js +3 -0
  3. package/dist/cli/index.js.map +1 -1
  4. package/dist/commands/init.d.ts +3 -0
  5. package/dist/commands/init.d.ts.map +1 -1
  6. package/dist/commands/init.js +48 -23
  7. package/dist/commands/init.js.map +1 -1
  8. package/dist/commands/update.d.ts.map +1 -1
  9. package/dist/commands/update.js +52 -41
  10. package/dist/commands/update.js.map +1 -1
  11. package/dist/configurators/codebuddy.d.ts +11 -0
  12. package/dist/configurators/codebuddy.d.ts.map +1 -0
  13. package/dist/configurators/codebuddy.js +58 -0
  14. package/dist/configurators/codebuddy.js.map +1 -0
  15. package/dist/configurators/codex.d.ts +7 -4
  16. package/dist/configurators/codex.d.ts.map +1 -1
  17. package/dist/configurators/codex.js +40 -10
  18. package/dist/configurators/codex.js.map +1 -1
  19. package/dist/configurators/copilot.d.ts +9 -0
  20. package/dist/configurators/copilot.d.ts.map +1 -0
  21. package/dist/configurators/copilot.js +34 -0
  22. package/dist/configurators/copilot.js.map +1 -0
  23. package/dist/configurators/index.d.ts +11 -1
  24. package/dist/configurators/index.d.ts.map +1 -1
  25. package/dist/configurators/index.js +72 -4
  26. package/dist/configurators/index.js.map +1 -1
  27. package/dist/configurators/opencode.d.ts +1 -1
  28. package/dist/configurators/opencode.js +1 -1
  29. package/dist/configurators/windsurf.d.ts +8 -0
  30. package/dist/configurators/windsurf.d.ts.map +1 -0
  31. package/dist/configurators/windsurf.js +18 -0
  32. package/dist/configurators/windsurf.js.map +1 -0
  33. package/dist/configurators/workflow.d.ts +1 -1
  34. package/dist/configurators/workflow.d.ts.map +1 -1
  35. package/dist/configurators/workflow.js +4 -2
  36. package/dist/configurators/workflow.js.map +1 -1
  37. package/dist/migrations/manifests/0.3.10.json +9 -0
  38. package/dist/migrations/manifests/0.4.0-beta.10.json +9 -0
  39. package/dist/migrations/manifests/0.4.0-beta.2.json +9 -0
  40. package/dist/migrations/manifests/0.4.0-beta.3.json +9 -0
  41. package/dist/migrations/manifests/0.4.0-beta.4.json +9 -0
  42. package/dist/migrations/manifests/0.4.0-beta.5.json +9 -0
  43. package/dist/migrations/manifests/0.4.0-beta.6.json +9 -0
  44. package/dist/migrations/manifests/0.4.0-beta.7.json +9 -0
  45. package/dist/migrations/manifests/0.4.0-beta.8.json +34 -0
  46. package/dist/migrations/manifests/0.4.0-beta.9.json +9 -0
  47. package/dist/templates/claude/commands/trellis/record-session.md +3 -2
  48. package/dist/templates/claude/hooks/inject-subagent-context.py +8 -1
  49. package/dist/templates/claude/hooks/ralph-loop.py +18 -10
  50. package/dist/templates/claude/hooks/session-start.py +33 -9
  51. package/dist/templates/claude/hooks/statusline.py +211 -0
  52. package/dist/templates/claude/settings.json +4 -0
  53. package/dist/templates/codebuddy/commands/trellis/before-dev.md +29 -0
  54. package/dist/templates/codebuddy/commands/trellis/brainstorm.md +487 -0
  55. package/dist/templates/codebuddy/commands/trellis/break-loop.md +107 -0
  56. package/dist/templates/codebuddy/commands/trellis/check-cross-layer.md +153 -0
  57. package/dist/templates/codebuddy/commands/trellis/check.md +25 -0
  58. package/dist/templates/codebuddy/commands/trellis/create-command.md +154 -0
  59. package/dist/templates/codebuddy/commands/trellis/finish-work.md +143 -0
  60. package/dist/templates/codebuddy/commands/trellis/integrate-skill.md +219 -0
  61. package/dist/templates/codebuddy/commands/trellis/onboard.md +358 -0
  62. package/dist/templates/codebuddy/commands/trellis/record-session.md +61 -0
  63. package/dist/templates/codebuddy/commands/trellis/start.md +373 -0
  64. package/dist/templates/codebuddy/commands/trellis/update-spec.md +354 -0
  65. package/dist/templates/codebuddy/index.d.ts +25 -0
  66. package/dist/templates/codebuddy/index.d.ts.map +1 -0
  67. package/dist/templates/codebuddy/index.js +45 -0
  68. package/dist/templates/codebuddy/index.js.map +1 -0
  69. package/dist/templates/codex/agents/check.toml +23 -0
  70. package/dist/templates/codex/agents/implement.toml +19 -0
  71. package/dist/templates/codex/agents/research.toml +26 -0
  72. package/dist/templates/codex/codex-skills/parallel/SKILL.md +194 -0
  73. package/dist/templates/codex/config.toml +5 -0
  74. package/dist/templates/codex/hooks/session-start.py +228 -0
  75. package/dist/templates/codex/hooks.json +16 -0
  76. package/dist/templates/codex/index.d.ts +27 -5
  77. package/dist/templates/codex/index.d.ts.map +1 -1
  78. package/dist/templates/codex/index.js +60 -8
  79. package/dist/templates/codex/index.js.map +1 -1
  80. package/dist/templates/codex/skills/before-dev/SKILL.md +1 -1
  81. package/dist/templates/codex/skills/brainstorm/SKILL.md +1 -1
  82. package/dist/templates/codex/skills/break-loop/SKILL.md +1 -1
  83. package/dist/templates/codex/skills/check/SKILL.md +1 -1
  84. package/dist/templates/codex/skills/check-cross-layer/SKILL.md +1 -1
  85. package/dist/templates/codex/skills/create-command/SKILL.md +1 -1
  86. package/dist/templates/codex/skills/finish-work/SKILL.md +1 -1
  87. package/dist/templates/codex/skills/improve-ut/SKILL.md +69 -0
  88. package/dist/templates/codex/skills/integrate-skill/SKILL.md +1 -1
  89. package/dist/templates/codex/skills/onboard/SKILL.md +1 -1
  90. package/dist/templates/codex/skills/record-session/SKILL.md +4 -3
  91. package/dist/templates/codex/skills/start/SKILL.md +1 -1
  92. package/dist/templates/codex/skills/update-spec/SKILL.md +1 -1
  93. package/dist/templates/copilot/hooks/session-start.py +218 -0
  94. package/dist/templates/copilot/hooks.json +11 -0
  95. package/dist/templates/copilot/index.d.ts +23 -0
  96. package/dist/templates/copilot/index.d.ts.map +1 -0
  97. package/dist/templates/copilot/index.js +54 -0
  98. package/dist/templates/copilot/index.js.map +1 -0
  99. package/dist/templates/copilot/prompts/before-dev.prompt.md +33 -0
  100. package/dist/templates/copilot/prompts/brainstorm.prompt.md +491 -0
  101. package/dist/templates/copilot/prompts/break-loop.prompt.md +129 -0
  102. package/dist/templates/copilot/prompts/check-cross-layer.prompt.md +157 -0
  103. package/dist/templates/copilot/prompts/check.prompt.md +29 -0
  104. package/dist/templates/copilot/prompts/create-command.prompt.md +116 -0
  105. package/dist/templates/copilot/prompts/finish-work.prompt.md +157 -0
  106. package/dist/templates/copilot/prompts/integrate-skill.prompt.md +223 -0
  107. package/dist/templates/copilot/prompts/onboard.prompt.md +362 -0
  108. package/dist/templates/copilot/prompts/parallel.prompt.md +196 -0
  109. package/dist/templates/copilot/prompts/record-session.prompt.md +66 -0
  110. package/dist/templates/copilot/prompts/start.prompt.md +397 -0
  111. package/dist/templates/copilot/prompts/update-spec.prompt.md +358 -0
  112. package/dist/templates/cursor/commands/trellis-record-session.md +3 -2
  113. package/dist/templates/extract.d.ts +36 -0
  114. package/dist/templates/extract.d.ts.map +1 -1
  115. package/dist/templates/extract.js +64 -0
  116. package/dist/templates/extract.js.map +1 -1
  117. package/dist/templates/gemini/commands/trellis/record-session.toml +3 -2
  118. package/dist/templates/iflow/commands/trellis/record-session.md +3 -2
  119. package/dist/templates/iflow/hooks/inject-subagent-context.py +8 -1
  120. package/dist/templates/iflow/hooks/ralph-loop.py +8 -1
  121. package/dist/templates/iflow/hooks/session-start.py +33 -9
  122. package/dist/templates/kilo/workflows/record-session.md +3 -2
  123. package/dist/templates/kiro/skills/before-dev/SKILL.md +1 -1
  124. package/dist/templates/kiro/skills/brainstorm/SKILL.md +1 -1
  125. package/dist/templates/kiro/skills/break-loop/SKILL.md +1 -1
  126. package/dist/templates/kiro/skills/check/SKILL.md +1 -1
  127. package/dist/templates/kiro/skills/check-cross-layer/SKILL.md +1 -1
  128. package/dist/templates/kiro/skills/create-command/SKILL.md +1 -1
  129. package/dist/templates/kiro/skills/finish-work/SKILL.md +1 -1
  130. package/dist/templates/kiro/skills/integrate-skill/SKILL.md +1 -1
  131. package/dist/templates/kiro/skills/onboard/SKILL.md +1 -1
  132. package/dist/templates/kiro/skills/record-session/SKILL.md +4 -3
  133. package/dist/templates/kiro/skills/start/SKILL.md +1 -1
  134. package/dist/templates/kiro/skills/update-spec/SKILL.md +1 -1
  135. package/dist/templates/markdown/agents.md +4 -0
  136. package/dist/templates/markdown/spec/backend/directory-structure.md +1 -1
  137. package/dist/templates/markdown/workspace-index.md +2 -0
  138. package/dist/templates/opencode/agents/dispatch.md +20 -19
  139. package/dist/templates/opencode/commands/trellis/record-session.md +3 -2
  140. package/dist/templates/opencode/lib/trellis-context.js +42 -2
  141. package/dist/templates/opencode/{plugin → plugins}/session-start.js +7 -27
  142. package/dist/templates/qoder/skills/before-dev/SKILL.md +1 -1
  143. package/dist/templates/qoder/skills/brainstorm/SKILL.md +1 -1
  144. package/dist/templates/qoder/skills/break-loop/SKILL.md +1 -1
  145. package/dist/templates/qoder/skills/check/SKILL.md +1 -1
  146. package/dist/templates/qoder/skills/check-cross-layer/SKILL.md +1 -1
  147. package/dist/templates/qoder/skills/create-command/SKILL.md +1 -1
  148. package/dist/templates/qoder/skills/finish-work/SKILL.md +1 -1
  149. package/dist/templates/qoder/skills/integrate-skill/SKILL.md +1 -1
  150. package/dist/templates/qoder/skills/onboard/SKILL.md +1 -1
  151. package/dist/templates/qoder/skills/record-session/SKILL.md +4 -3
  152. package/dist/templates/qoder/skills/start/SKILL.md +1 -1
  153. package/dist/templates/qoder/skills/update-spec/SKILL.md +1 -1
  154. package/dist/templates/trellis/scripts/add_session.py +69 -16
  155. package/dist/templates/trellis/scripts/common/__init__.py +2 -0
  156. package/dist/templates/trellis/scripts/common/cli_adapter.py +133 -21
  157. package/dist/templates/trellis/scripts/common/config.py +40 -0
  158. package/dist/templates/trellis/scripts/common/developer.py +2 -2
  159. package/dist/templates/trellis/scripts/common/packages_context.py +9 -4
  160. package/dist/templates/trellis/scripts/common/paths.py +57 -6
  161. package/dist/templates/trellis/scripts/common/session_context.py +98 -2
  162. package/dist/templates/trellis/scripts/common/task_context.py +27 -1
  163. package/dist/templates/trellis/scripts/common/task_store.py +6 -4
  164. package/dist/templates/trellis/scripts/common/task_utils.py +14 -8
  165. package/dist/templates/trellis/scripts/create_bootstrap.py +1 -1
  166. package/dist/templates/trellis/scripts/multi_agent/plan.py +7 -6
  167. package/dist/templates/trellis/scripts/multi_agent/start.py +16 -11
  168. package/dist/templates/trellis/scripts/task.py +1 -1
  169. package/dist/templates/trellis/scripts-shell-archive/create-bootstrap.sh +1 -1
  170. package/dist/templates/trellis/workflow.md +17 -4
  171. package/dist/templates/windsurf/index.d.ts +21 -0
  172. package/dist/templates/windsurf/index.d.ts.map +1 -0
  173. package/dist/templates/windsurf/index.js +44 -0
  174. package/dist/templates/windsurf/index.js.map +1 -0
  175. package/dist/templates/windsurf/workflows/trellis-before-dev.md +31 -0
  176. package/dist/templates/windsurf/workflows/trellis-brainstorm.md +491 -0
  177. package/dist/templates/windsurf/workflows/trellis-break-loop.md +111 -0
  178. package/dist/templates/windsurf/workflows/trellis-check-cross-layer.md +157 -0
  179. package/dist/templates/windsurf/workflows/trellis-check.md +27 -0
  180. package/dist/templates/windsurf/workflows/trellis-create-command.md +154 -0
  181. package/dist/templates/windsurf/workflows/trellis-finish-work.md +147 -0
  182. package/dist/templates/windsurf/workflows/trellis-integrate-skill.md +220 -0
  183. package/dist/templates/windsurf/workflows/trellis-onboard.md +362 -0
  184. package/dist/templates/windsurf/workflows/trellis-record-session.md +66 -0
  185. package/dist/templates/windsurf/workflows/trellis-start.md +373 -0
  186. package/dist/templates/windsurf/workflows/trellis-update-spec.md +358 -0
  187. package/dist/types/ai-tools.d.ts +15 -3
  188. package/dist/types/ai-tools.d.ts.map +1 -1
  189. package/dist/types/ai-tools.js +42 -2
  190. package/dist/types/ai-tools.js.map +1 -1
  191. package/dist/utils/project-detector.d.ts +5 -0
  192. package/dist/utils/project-detector.d.ts.map +1 -1
  193. package/dist/utils/project-detector.js +7 -0
  194. package/dist/utils/project-detector.js.map +1 -1
  195. package/dist/utils/template-fetcher.d.ts +24 -3
  196. package/dist/utils/template-fetcher.d.ts.map +1 -1
  197. package/dist/utils/template-fetcher.js +129 -16
  198. package/dist/utils/template-fetcher.js.map +1 -1
  199. package/package.json +1 -1
  200. /package/dist/templates/opencode/{plugin → plugins}/inject-subagent-context.js +0 -0
@@ -13,7 +13,7 @@ from __future__ import annotations
13
13
 
14
14
  from pathlib import Path
15
15
 
16
- from .config import get_default_package, get_packages, get_spec_scope
16
+ from .config import _is_true_config_value, get_default_package, get_packages, get_spec_scope
17
17
  from .paths import (
18
18
  DIR_SPEC,
19
19
  DIR_WORKFLOW,
@@ -91,7 +91,8 @@ def _resolve_scope_set(
91
91
  def get_packages_info(repo_root: Path) -> list[dict]:
92
92
  """Get structured package info for monorepo projects.
93
93
 
94
- Returns list of dicts with keys: name, path, type, default, specLayers, isSubmodule.
94
+ Returns list of dicts with keys: name, path, type, default, specLayers,
95
+ isSubmodule, isGitRepo.
95
96
  Returns empty list for single-repo projects.
96
97
  """
97
98
  packages = get_packages(repo_root)
@@ -105,6 +106,7 @@ def get_packages_info(repo_root: Path) -> list[dict]:
105
106
  for pkg_name, pkg_config in packages.items():
106
107
  pkg_path = pkg_config.get("path", pkg_name) if isinstance(pkg_config, dict) else str(pkg_config)
107
108
  pkg_type = pkg_config.get("type", "local") if isinstance(pkg_config, dict) else "local"
109
+ pkg_git = pkg_config.get("git", False) if isinstance(pkg_config, dict) else False
108
110
  layers = _scan_spec_layers(spec_dir, pkg_name)
109
111
 
110
112
  result.append({
@@ -114,6 +116,7 @@ def get_packages_info(repo_root: Path) -> list[dict]:
114
116
  "default": pkg_name == default_pkg,
115
117
  "specLayers": layers,
116
118
  "isSubmodule": pkg_type == "submodule",
119
+ "isGitRepo": _is_true_config_value(pkg_git),
117
120
  })
118
121
 
119
122
  return result
@@ -139,9 +142,10 @@ def get_packages_section(repo_root: Path) -> str:
139
142
  for pkg in pkg_info:
140
143
  layers_str = f" [{', '.join(pkg['specLayers'])}]" if pkg["specLayers"] else ""
141
144
  submodule_tag = " (submodule)" if pkg["isSubmodule"] else ""
145
+ git_repo_tag = " (git repo)" if pkg["isGitRepo"] else ""
142
146
  default_tag = " *" if pkg["default"] else ""
143
147
  lines.append(
144
- f"- {pkg['name']:<16} {pkg['path']:<20}{layers_str}{submodule_tag}{default_tag}"
148
+ f"- {pkg['name']:<16} {pkg['path']:<20}{layers_str}{submodule_tag}{git_repo_tag}{default_tag}"
145
149
  )
146
150
 
147
151
  if default_pkg:
@@ -179,13 +183,14 @@ def get_context_packages_text(repo_root: Path | None = None) -> str:
179
183
  for pkg in pkg_info:
180
184
  default_tag = " (default)" if pkg["default"] else ""
181
185
  type_tag = f" [{pkg['type']}]" if pkg["type"] != "local" else ""
186
+ git_tag = " [git repo]" if pkg["isGitRepo"] else ""
182
187
 
183
188
  # Scope annotation
184
189
  scope_tag = ""
185
190
  if scope_set is not None and pkg["name"] not in scope_set:
186
191
  scope_tag = " (out of scope)"
187
192
 
188
- lines.append(f"### {pkg['name']}{default_tag}{type_tag}{scope_tag}")
193
+ lines.append(f"### {pkg['name']}{default_tag}{type_tag}{git_tag}{scope_tag}")
189
194
  lines.append(f"Path: {pkg['path']}")
190
195
  if pkg["specLayers"]:
191
196
  lines.append(f"Spec layers: {', '.join(pkg['specLayers'])}")
@@ -221,6 +221,50 @@ def _get_current_task_file(repo_root: Path | None = None) -> Path:
221
221
  return repo_root / DIR_WORKFLOW / FILE_CURRENT_TASK
222
222
 
223
223
 
224
+ def normalize_task_ref(task_ref: str) -> str:
225
+ """Normalize a task ref for stable storage in .current-task.
226
+
227
+ Stored refs should prefer repo-relative POSIX paths like
228
+ `.trellis/tasks/03-27-my-task`, even on Windows. Absolute paths are preserved
229
+ unless they can later be converted back to repo-relative form by callers.
230
+ """
231
+ normalized = task_ref.strip()
232
+ if not normalized:
233
+ return ""
234
+
235
+ path_obj = Path(normalized)
236
+ if path_obj.is_absolute():
237
+ return str(path_obj)
238
+
239
+ normalized = normalized.replace("\\", "/")
240
+ while normalized.startswith("./"):
241
+ normalized = normalized[2:]
242
+
243
+ if normalized.startswith(f"{DIR_TASKS}/"):
244
+ return f"{DIR_WORKFLOW}/{normalized}"
245
+
246
+ return normalized
247
+
248
+
249
+ def resolve_task_ref(task_ref: str, repo_root: Path | None = None) -> Path | None:
250
+ """Resolve a task ref from .current-task to an absolute task directory path."""
251
+ if repo_root is None:
252
+ repo_root = get_repo_root()
253
+
254
+ normalized = normalize_task_ref(task_ref)
255
+ if not normalized:
256
+ return None
257
+
258
+ path_obj = Path(normalized)
259
+ if path_obj.is_absolute():
260
+ return path_obj
261
+
262
+ if normalized.startswith(f"{DIR_WORKFLOW}/"):
263
+ return repo_root / path_obj
264
+
265
+ return repo_root / DIR_WORKFLOW / DIR_TASKS / path_obj
266
+
267
+
224
268
  def get_current_task(repo_root: Path | None = None) -> str | None:
225
269
  """Get current task directory path (relative to repo_root).
226
270
 
@@ -236,7 +280,8 @@ def get_current_task(repo_root: Path | None = None) -> str | None:
236
280
  return None
237
281
 
238
282
  try:
239
- return current_file.read_text(encoding="utf-8").strip()
283
+ content = current_file.read_text(encoding="utf-8").strip()
284
+ return normalize_task_ref(content) if content else None
240
285
  except (OSError, IOError):
241
286
  return None
242
287
 
@@ -255,7 +300,7 @@ def get_current_task_abs(repo_root: Path | None = None) -> Path | None:
255
300
 
256
301
  relative = get_current_task(repo_root)
257
302
  if relative:
258
- return repo_root / relative
303
+ return resolve_task_ref(relative, repo_root)
259
304
  return None
260
305
 
261
306
 
@@ -272,18 +317,24 @@ def set_current_task(task_path: str, repo_root: Path | None = None) -> bool:
272
317
  if repo_root is None:
273
318
  repo_root = get_repo_root()
274
319
 
275
- if not task_path:
320
+ normalized = normalize_task_ref(task_path)
321
+ if not normalized:
276
322
  return False
277
323
 
278
324
  # Verify task directory exists
279
- full_path = repo_root / task_path
280
- if not full_path.is_dir():
325
+ full_path = resolve_task_ref(normalized, repo_root)
326
+ if full_path is None or not full_path.is_dir():
281
327
  return False
282
328
 
329
+ try:
330
+ normalized = full_path.relative_to(repo_root).as_posix()
331
+ except ValueError:
332
+ normalized = str(full_path)
333
+
283
334
  current_file = _get_current_task_file(repo_root)
284
335
 
285
336
  try:
286
- current_file.write_text(task_path, encoding="utf-8")
337
+ current_file.write_text(normalized, encoding="utf-8")
287
338
  return True
288
339
  except (OSError, IOError):
289
340
  return False
@@ -16,6 +16,7 @@ from __future__ import annotations
16
16
  import json
17
17
  from pathlib import Path
18
18
 
19
+ from .config import get_git_packages
19
20
  from .git import run_git
20
21
  from .packages_context import get_packages_section
21
22
  from .tasks import iter_active_tasks, load_task, get_all_statuses, children_progress
@@ -34,6 +35,79 @@ from .paths import (
34
35
  )
35
36
 
36
37
 
38
+ # =============================================================================
39
+ # Helpers
40
+ # =============================================================================
41
+
42
+ def _collect_package_git_info(repo_root: Path) -> list[dict]:
43
+ """Collect git status and recent commits for packages with independent git repos.
44
+
45
+ Only packages marked with ``git: true`` in config.yaml are included.
46
+
47
+ Returns:
48
+ List of dicts with keys: name, path, branch, isClean,
49
+ uncommittedChanges, recentCommits.
50
+ Empty list if no git-repo packages are configured.
51
+ """
52
+ git_pkgs = get_git_packages(repo_root)
53
+ if not git_pkgs:
54
+ return []
55
+
56
+ result = []
57
+ for pkg_name, pkg_path in git_pkgs.items():
58
+ pkg_dir = repo_root / pkg_path
59
+ if not (pkg_dir / ".git").exists():
60
+ continue
61
+
62
+ _, branch_out, _ = run_git(["branch", "--show-current"], cwd=pkg_dir)
63
+ branch = branch_out.strip() or "unknown"
64
+
65
+ _, status_out, _ = run_git(["status", "--porcelain"], cwd=pkg_dir)
66
+ changes = len([l for l in status_out.splitlines() if l.strip()])
67
+
68
+ _, log_out, _ = run_git(["log", "--oneline", "-5"], cwd=pkg_dir)
69
+ commits = []
70
+ for line in log_out.splitlines():
71
+ if line.strip():
72
+ parts = line.split(" ", 1)
73
+ if len(parts) >= 2:
74
+ commits.append({"hash": parts[0], "message": parts[1]})
75
+ elif len(parts) == 1:
76
+ commits.append({"hash": parts[0], "message": ""})
77
+
78
+ result.append({
79
+ "name": pkg_name,
80
+ "path": pkg_path,
81
+ "branch": branch,
82
+ "isClean": changes == 0,
83
+ "uncommittedChanges": changes,
84
+ "recentCommits": commits,
85
+ })
86
+
87
+ return result
88
+
89
+
90
+ def _append_package_git_context(lines: list[str], package_git_info: list[dict]) -> None:
91
+ """Append Git status and recent commits for package repositories."""
92
+ for pkg in package_git_info:
93
+ lines.append(f"## GIT STATUS ({pkg['name']}: {pkg['path']})")
94
+ lines.append(f"Branch: {pkg['branch']}")
95
+ if pkg["isClean"]:
96
+ lines.append("Working directory: Clean")
97
+ else:
98
+ lines.append(
99
+ f"Working directory: {pkg['uncommittedChanges']} uncommitted change(s)"
100
+ )
101
+ lines.append("")
102
+ lines.append(f"## RECENT COMMITS ({pkg['name']}: {pkg['path']})")
103
+ if pkg["recentCommits"]:
104
+ for commit in pkg["recentCommits"]:
105
+ lines.append(f"{commit['hash']} {commit['message']}")
106
+ else:
107
+ lines.append("(no commits)")
108
+ lines.append("")
109
+
110
+
37
111
  # =============================================================================
38
112
  # JSON Output
39
113
  # =============================================================================
@@ -93,7 +167,10 @@ def get_context_json(repo_root: Path | None = None) -> dict:
93
167
  for t in iter_active_tasks(tasks_dir)
94
168
  ]
95
169
 
96
- return {
170
+ # Package git repos (independent sub-repositories)
171
+ pkg_git_info = _collect_package_git_info(repo_root)
172
+
173
+ result = {
97
174
  "developer": developer or "",
98
175
  "git": {
99
176
  "branch": branch,
@@ -112,6 +189,11 @@ def get_context_json(repo_root: Path | None = None) -> dict:
112
189
  },
113
190
  }
114
191
 
192
+ if pkg_git_info:
193
+ result["packageGit"] = pkg_git_info
194
+
195
+ return result
196
+
115
197
 
116
198
  def output_json(repo_root: Path | None = None) -> None:
117
199
  """Output context in JSON format.
@@ -189,6 +271,9 @@ def get_context_text(repo_root: Path | None = None) -> str:
189
271
  lines.append("(no commits)")
190
272
  lines.append("")
191
273
 
274
+ # Package git repos — independent sub-repositories
275
+ _append_package_git_context(lines, _collect_package_git_info(repo_root))
276
+
192
277
  # Current task
193
278
  lines.append("## CURRENT TASK")
194
279
  current_task = get_current_task(repo_root)
@@ -352,7 +437,10 @@ def get_context_record_json(repo_root: Path | None = None) -> dict:
352
437
  "status": ct.status,
353
438
  }
354
439
 
355
- return {
440
+ # Package git repos
441
+ pkg_git_info = _collect_package_git_info(repo_root)
442
+
443
+ result = {
356
444
  "developer": developer or "",
357
445
  "git": {
358
446
  "branch": branch,
@@ -364,6 +452,11 @@ def get_context_record_json(repo_root: Path | None = None) -> dict:
364
452
  "currentTask": current_task_info,
365
453
  }
366
454
 
455
+ if pkg_git_info:
456
+ result["packageGit"] = pkg_git_info
457
+
458
+ return result
459
+
367
460
 
368
461
  def get_context_text_record(repo_root: Path | None = None) -> str:
369
462
  """Get context as formatted text for record-session mode.
@@ -439,6 +532,9 @@ def get_context_text_record(repo_root: Path | None = None) -> str:
439
532
  lines.append("(no commits)")
440
533
  lines.append("")
441
534
 
535
+ # Package git repos — independent sub-repositories
536
+ _append_package_git_context(lines, _collect_package_git_info(repo_root))
537
+
442
538
  # CURRENT TASK
443
539
  lines.append("## CURRENT TASK")
444
540
  current_task = get_current_task(repo_root)
@@ -194,8 +194,34 @@ def cmd_init_context(args: argparse.Namespace) -> int:
194
194
  print()
195
195
  print(colored("✓ All context files created", Colors.GREEN))
196
196
  print()
197
+
198
+ # Show what was auto-injected
199
+ all_injected = [e["file"] for e in implement_entries]
200
+ print(colored("Auto-injected (defaults only):", Colors.YELLOW))
201
+ for f in all_injected:
202
+ print(f" - {f}")
203
+ print()
204
+
205
+ # Scan spec directory for available spec files the AI should consider
206
+ spec_base = repo_root / DIR_WORKFLOW / DIR_SPEC
207
+ if package:
208
+ spec_base = spec_base / package
209
+ available_specs: list[str] = []
210
+ if spec_base.is_dir():
211
+ for md_file in sorted(spec_base.rglob("*.md")):
212
+ rel = str(md_file.relative_to(repo_root))
213
+ if rel not in all_injected:
214
+ available_specs.append(rel)
215
+
216
+ if available_specs:
217
+ print(colored("Available spec files (not yet injected):", Colors.BLUE))
218
+ for spec in available_specs:
219
+ print(f" - {spec}")
220
+ print()
221
+
197
222
  print(colored("Next steps:", Colors.BLUE))
198
- print(" 1. Add task-specific specs: python3 task.py add-context <dir> <jsonl> <path>")
223
+ print(" 1. Review the spec files above and add relevant ones for your task:")
224
+ print(f" python3 task.py add-context <dir> implement <spec-path> \"<reason>\"")
199
225
  print(" 2. Set as current: python3 task.py start <dir>")
200
226
 
201
227
  return 0
@@ -163,10 +163,12 @@ def cmd_create(args: argparse.Namespace) -> int:
163
163
  "worktree_path": None,
164
164
  "current_phase": 0,
165
165
  "next_action": [
166
- {"phase": 1, "action": "implement"},
167
- {"phase": 2, "action": "check"},
168
- {"phase": 3, "action": "finish"},
169
- {"phase": 4, "action": "create-pr"},
166
+ {"phase": 1, "action": "brainstorm"},
167
+ {"phase": 2, "action": "research"},
168
+ {"phase": 3, "action": "implement"},
169
+ {"phase": 4, "action": "check"},
170
+ {"phase": 5, "action": "update-spec"},
171
+ {"phase": 6, "action": "record-session"},
170
172
  ],
171
173
  "commit": None,
172
174
  "pr_url": None,
@@ -37,23 +37,25 @@ def is_safe_task_path(task_path: str, repo_root: Path | None = None) -> bool:
37
37
  if repo_root is None:
38
38
  repo_root = get_repo_root()
39
39
 
40
+ normalized = task_path.replace("\\", "/")
41
+
40
42
  # Check empty or null
41
- if not task_path or task_path == "null":
43
+ if not normalized or normalized == "null":
42
44
  print("Error: empty or null task path", file=sys.stderr)
43
45
  return False
44
46
 
45
47
  # Reject absolute paths
46
- if task_path.startswith("/"):
48
+ if Path(task_path).is_absolute():
47
49
  print(f"Error: absolute path not allowed: {task_path}", file=sys.stderr)
48
50
  return False
49
51
 
50
52
  # Reject ".", "..", paths starting with "./" or "../", or containing ".."
51
- if task_path in (".", "..") or task_path.startswith("./") or task_path.startswith("../") or ".." in task_path:
53
+ if normalized in (".", "..") or normalized.startswith("./") or normalized.startswith("../") or ".." in normalized:
52
54
  print(f"Error: path traversal not allowed: {task_path}", file=sys.stderr)
53
55
  return False
54
56
 
55
57
  # Final check: ensure resolved path is not the repo root
56
- abs_path = repo_root / task_path
58
+ abs_path = repo_root / Path(normalized)
57
59
  if abs_path.exists():
58
60
  try:
59
61
  resolved = abs_path.resolve()
@@ -187,13 +189,17 @@ def resolve_task_dir(target_dir: str, repo_root: Path) -> Path:
187
189
  if not target_dir:
188
190
  return Path()
189
191
 
192
+ normalized = target_dir.replace("\\", "/")
193
+ while normalized.startswith("./"):
194
+ normalized = normalized[2:]
195
+
190
196
  # Absolute path
191
- if target_dir.startswith("/"):
197
+ if Path(target_dir).is_absolute():
192
198
  return Path(target_dir)
193
199
 
194
200
  # Relative path (contains path separator or starts with .trellis)
195
- if "/" in target_dir or target_dir.startswith(".trellis"):
196
- return repo_root / target_dir
201
+ if "/" in normalized or normalized.startswith(".trellis"):
202
+ return repo_root / Path(normalized)
197
203
 
198
204
  # Task name - try to find in tasks directory
199
205
  tasks_dir = get_tasks_dir(repo_root)
@@ -202,7 +208,7 @@ def resolve_task_dir(target_dir: str, repo_root: Path) -> Path:
202
208
  return found
203
209
 
204
210
  # Fallback to treating as relative path
205
- return repo_root / target_dir
211
+ return repo_root / Path(normalized)
206
212
 
207
213
 
208
214
  # =============================================================================
@@ -59,7 +59,7 @@ def write_prd_header() -> str:
59
59
  Welcome to Trellis! This is your first task.
60
60
 
61
61
  AI agents use `.trellis/spec/` to understand YOUR project's coding conventions.
62
- **Empty templates = AI writes generic code that doesn't match your project style.**
62
+ **Starting from scratch = AI writes generic code that doesn't match your project style.**
63
63
 
64
64
  Filling these guidelines is a one-time setup that pays off for every future AI session.
65
65
 
@@ -53,7 +53,7 @@ def main() -> int:
53
53
  parser.add_argument("--requirement", "-r", required=True, help="Requirement description")
54
54
  parser.add_argument(
55
55
  "--platform", "-p",
56
- choices=["claude", "cursor", "iflow", "opencode", "qoder"],
56
+ choices=["claude", "cursor", "iflow", "opencode", "codex", "qoder"],
57
57
  default=DEFAULT_PLATFORM,
58
58
  help="Platform to use (default: claude)"
59
59
  )
@@ -76,11 +76,12 @@ def main() -> int:
76
76
  project_root = get_repo_root()
77
77
 
78
78
  # Check plan agent exists (path varies by platform)
79
- plan_md = adapter.get_agent_path("plan", project_root)
80
- if not plan_md.is_file():
81
- log_error(f"plan agent not found at {plan_md}")
82
- log_info(f"Platform: {platform}")
83
- return 1
79
+ if adapter.requires_agent_definition_file:
80
+ plan_md = adapter.get_agent_path("plan", project_root)
81
+ if not plan_md.is_file():
82
+ log_error(f"Agent definition not found at {plan_md}")
83
+ log_info(f"Platform: {platform}")
84
+ return 1
84
85
 
85
86
  ensure_developer(project_root)
86
87
 
@@ -187,7 +187,7 @@ def main() -> int:
187
187
  parser.add_argument("task_dir", help="Task directory path")
188
188
  parser.add_argument(
189
189
  "--platform", "-p",
190
- choices=["claude", "cursor", "iflow", "opencode", "qoder"],
190
+ choices=["claude", "cursor", "iflow", "opencode", "codex", "qoder"],
191
191
  default=DEFAULT_PLATFORM,
192
192
  help="Platform to use (default: claude)"
193
193
  )
@@ -202,12 +202,16 @@ def main() -> int:
202
202
  project_root = get_repo_root()
203
203
 
204
204
  # Normalize paths
205
- if task_dir_arg.startswith("/"):
206
- task_dir_relative = task_dir_arg[len(str(project_root)) + 1 :]
207
- task_dir_abs = Path(task_dir_arg)
205
+ task_dir_path = Path(task_dir_arg)
206
+ if task_dir_path.is_absolute():
207
+ task_dir_abs = task_dir_path
208
208
  else:
209
- task_dir_relative = task_dir_arg
210
- task_dir_abs = project_root / task_dir_arg
209
+ task_dir_abs = project_root / task_dir_path
210
+
211
+ try:
212
+ task_dir_relative = task_dir_abs.relative_to(project_root).as_posix()
213
+ except ValueError:
214
+ task_dir_relative = str(task_dir_abs)
211
215
 
212
216
  task_json_path = task_dir_abs / FILE_TASK_JSON
213
217
 
@@ -218,11 +222,12 @@ def main() -> int:
218
222
  log_error(f"task.json not found at {task_json_path}")
219
223
  return 1
220
224
 
221
- dispatch_md = adapter.get_agent_path("dispatch", project_root)
222
- if not dispatch_md.is_file():
223
- log_error(f"dispatch.md not found at {dispatch_md}")
224
- log_info(f"Platform: {platform}")
225
- return 1
225
+ if adapter.requires_agent_definition_file:
226
+ dispatch_md = adapter.get_agent_path("dispatch", project_root)
227
+ if not dispatch_md.is_file():
228
+ log_error(f"Agent definition not found at {dispatch_md}")
229
+ log_info(f"Platform: {platform}")
230
+ return 1
226
231
 
227
232
  config_file = get_worktree_config(project_root)
228
233
  if not config_file.is_file():
@@ -84,7 +84,7 @@ def cmd_start(args: argparse.Namespace) -> int:
84
84
 
85
85
  # Convert to relative path for storage
86
86
  try:
87
- task_dir = str(full_path.relative_to(repo_root))
87
+ task_dir = full_path.relative_to(repo_root).as_posix()
88
88
  except ValueError:
89
89
  task_dir = str(full_path)
90
90
 
@@ -59,7 +59,7 @@ write_prd_header() {
59
59
  Welcome to Trellis! This is your first task.
60
60
 
61
61
  AI agents use `.trellis/spec/` to understand YOUR project's coding conventions.
62
- **Empty templates = AI writes generic code that doesn't match your project style.**
62
+ **Starting from scratch = AI writes generic code that doesn't match your project style.**
63
63
 
64
64
  Filling these guidelines is a one-time setup that pays off for every future AI session.
65
65
 
@@ -196,21 +196,30 @@ python3 ./.trellis/scripts/task.py create "<title>" --slug <task-name>
196
196
  1. Create or select task
197
197
  --> python3 ./.trellis/scripts/task.py create "<title>" --slug <name> or list
198
198
 
199
- 2. Write code according to guidelines
199
+ 2. Start task (mark as current)
200
+ --> python3 ./.trellis/scripts/task.py start <name>
201
+ --> Writes .trellis/.current-task; future sessions see it in <current-state>
202
+
203
+ 3. Write code according to guidelines
200
204
  --> Read .trellis/spec/ docs relevant to your task
201
205
  --> For cross-layer: read .trellis/spec/guides/
202
206
 
203
- 3. Self-test
207
+ 4. Self-test
204
208
  --> Run project's lint/test commands (see spec docs)
205
209
  --> Manual feature testing
206
210
 
207
- 4. Commit code
211
+ 5. Commit code
208
212
  --> git add <files>
209
213
  --> git commit -m "type(scope): description"
210
214
  Format: feat/fix/docs/refactor/test/chore
211
215
 
212
- 5. Record session (one command)
216
+ 6. Record session (one command)
213
217
  --> python3 ./.trellis/scripts/add_session.py --title "Title" --commit "hash"
218
+
219
+ 7. Finish task (clear current)
220
+ --> python3 ./.trellis/scripts/task.py finish
221
+ --> Only when the task is fully done; otherwise leave it set so the
222
+ next session resumes where you left off
214
223
  ```
215
224
 
216
225
  ### Code Quality Checklist
@@ -315,11 +324,15 @@ tasks/
315
324
  **Commands**:
316
325
  ```bash
317
326
  python3 ./.trellis/scripts/task.py create "<title>" [--slug <name>] # Create task directory
327
+ python3 ./.trellis/scripts/task.py start <name> # Set as current task (writes .current-task, triggers after_start hooks)
328
+ python3 ./.trellis/scripts/task.py finish # Clear current task (triggers after_finish hooks)
318
329
  python3 ./.trellis/scripts/task.py archive <name> # Archive to archive/{year-month}/
319
330
  python3 ./.trellis/scripts/task.py list # List active tasks
320
331
  python3 ./.trellis/scripts/task.py list-archive # List archived tasks
321
332
  ```
322
333
 
334
+ **Current task mechanism**: `task.py start <name>` writes the selected task path to `.trellis/.current-task`. The SessionStart hook reads this file to inject `## CURRENT TASK` into every new session's context, so the AI immediately knows what you're working on without being told. Run `task.py finish` when you're done — subsequent sessions will show `(none)` until you start another task.
335
+
323
336
  ---
324
337
 
325
338
  ## Best Practices
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Windsurf workflow templates
3
+ *
4
+ * These are GENERIC templates for user projects.
5
+ * Do NOT use Trellis project's own .windsurf/ directory (which may be customized).
6
+ *
7
+ * Directory structure:
8
+ * windsurf/
9
+ * └── workflows/ # Workflow files
10
+ */
11
+ export interface WorkflowTemplate {
12
+ name: string;
13
+ content: string;
14
+ }
15
+ /**
16
+ * Get all workflow templates.
17
+ * Workflow names match their filename stem
18
+ * (e.g. trellis-start.md -> /trellis-start).
19
+ */
20
+ export declare function getAllWorkflows(): WorkflowTemplate[];
21
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/templates/windsurf/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAqBH,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,IAAI,gBAAgB,EAAE,CAcpD"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Windsurf workflow templates
3
+ *
4
+ * These are GENERIC templates for user projects.
5
+ * Do NOT use Trellis project's own .windsurf/ directory (which may be customized).
6
+ *
7
+ * Directory structure:
8
+ * windsurf/
9
+ * └── workflows/ # Workflow files
10
+ */
11
+ import { readdirSync, readFileSync } from "node:fs";
12
+ import { dirname, join } from "node:path";
13
+ import { fileURLToPath } from "node:url";
14
+ const __filename = fileURLToPath(import.meta.url);
15
+ const __dirname = dirname(__filename);
16
+ function readTemplate(relativePath) {
17
+ return readFileSync(join(__dirname, relativePath), "utf-8");
18
+ }
19
+ function listFiles(dir) {
20
+ try {
21
+ return readdirSync(join(__dirname, dir));
22
+ }
23
+ catch {
24
+ return [];
25
+ }
26
+ }
27
+ /**
28
+ * Get all workflow templates.
29
+ * Workflow names match their filename stem
30
+ * (e.g. trellis-start.md -> /trellis-start).
31
+ */
32
+ export function getAllWorkflows() {
33
+ const workflows = [];
34
+ for (const file of listFiles("workflows")) {
35
+ if (!file.endsWith(".md")) {
36
+ continue;
37
+ }
38
+ const name = file.replace(".md", "");
39
+ const content = readTemplate(`workflows/${file}`);
40
+ workflows.push({ name, content });
41
+ }
42
+ return workflows;
43
+ }
44
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/templates/windsurf/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAClD,MAAM,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;AAEtC,SAAS,YAAY,CAAC,YAAoB;IACxC,OAAO,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,YAAY,CAAC,EAAE,OAAO,CAAC,CAAC;AAC9D,CAAC;AAED,SAAS,SAAS,CAAC,GAAW;IAC5B,IAAI,CAAC;QACH,OAAO,WAAW,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC,CAAC;IAC3C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAOD;;;;GAIG;AACH,MAAM,UAAU,eAAe;IAC7B,MAAM,SAAS,GAAuB,EAAE,CAAC;IAEzC,KAAK,MAAM,IAAI,IAAI,SAAS,CAAC,WAAW,CAAC,EAAE,CAAC;QAC1C,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;YAC1B,SAAS;QACX,CAAC;QAED,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QACrC,MAAM,OAAO,GAAG,YAAY,CAAC,aAAa,IAAI,EAAE,CAAC,CAAC;QAClD,SAAS,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;IACpC,CAAC;IAED,OAAO,SAAS,CAAC;AACnB,CAAC"}