@mindfoldhq/trellis 0.4.0-beta.8 → 0.4.0-rc.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 (99) hide show
  1. package/README.md +10 -5
  2. package/dist/cli/index.js +2 -0
  3. package/dist/cli/index.js.map +1 -1
  4. package/dist/commands/init.d.ts +2 -0
  5. package/dist/commands/init.d.ts.map +1 -1
  6. package/dist/commands/init.js +165 -13
  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 +14 -2
  10. package/dist/commands/update.js.map +1 -1
  11. package/dist/configurators/codex.d.ts.map +1 -1
  12. package/dist/configurators/codex.js +2 -1
  13. package/dist/configurators/codex.js.map +1 -1
  14. package/dist/configurators/copilot.d.ts +9 -0
  15. package/dist/configurators/copilot.d.ts.map +1 -0
  16. package/dist/configurators/copilot.js +34 -0
  17. package/dist/configurators/copilot.js.map +1 -0
  18. package/dist/configurators/index.d.ts.map +1 -1
  19. package/dist/configurators/index.js +32 -1
  20. package/dist/configurators/index.js.map +1 -1
  21. package/dist/configurators/windsurf.d.ts +8 -0
  22. package/dist/configurators/windsurf.d.ts.map +1 -0
  23. package/dist/configurators/windsurf.js +18 -0
  24. package/dist/configurators/windsurf.js.map +1 -0
  25. package/dist/migrations/manifests/0.4.0-beta.10.json +9 -0
  26. package/dist/migrations/manifests/0.4.0-beta.9.json +9 -0
  27. package/dist/migrations/manifests/0.4.0-rc.0.json +9 -0
  28. package/dist/templates/claude/hooks/inject-subagent-context.py +8 -1
  29. package/dist/templates/claude/hooks/ralph-loop.py +18 -10
  30. package/dist/templates/claude/hooks/session-start.py +60 -19
  31. package/dist/templates/claude/hooks/statusline.py +218 -0
  32. package/dist/templates/claude/settings.json +4 -0
  33. package/dist/templates/codex/hooks/session-start.py +60 -21
  34. package/dist/templates/codex/hooks.json +1 -1
  35. package/dist/templates/copilot/hooks/session-start.py +243 -0
  36. package/dist/templates/copilot/hooks.json +11 -0
  37. package/dist/templates/copilot/index.d.ts +23 -0
  38. package/dist/templates/copilot/index.d.ts.map +1 -0
  39. package/dist/templates/copilot/index.js +54 -0
  40. package/dist/templates/copilot/index.js.map +1 -0
  41. package/dist/templates/copilot/prompts/before-dev.prompt.md +33 -0
  42. package/dist/templates/copilot/prompts/brainstorm.prompt.md +491 -0
  43. package/dist/templates/copilot/prompts/break-loop.prompt.md +129 -0
  44. package/dist/templates/copilot/prompts/check-cross-layer.prompt.md +157 -0
  45. package/dist/templates/copilot/prompts/check.prompt.md +29 -0
  46. package/dist/templates/copilot/prompts/create-command.prompt.md +116 -0
  47. package/dist/templates/copilot/prompts/finish-work.prompt.md +157 -0
  48. package/dist/templates/copilot/prompts/integrate-skill.prompt.md +223 -0
  49. package/dist/templates/copilot/prompts/onboard.prompt.md +362 -0
  50. package/dist/templates/copilot/prompts/parallel.prompt.md +196 -0
  51. package/dist/templates/copilot/prompts/record-session.prompt.md +66 -0
  52. package/dist/templates/copilot/prompts/start.prompt.md +397 -0
  53. package/dist/templates/copilot/prompts/update-spec.prompt.md +358 -0
  54. package/dist/templates/extract.d.ts +18 -0
  55. package/dist/templates/extract.d.ts.map +1 -1
  56. package/dist/templates/extract.js +32 -0
  57. package/dist/templates/extract.js.map +1 -1
  58. package/dist/templates/iflow/hooks/inject-subagent-context.py +8 -1
  59. package/dist/templates/iflow/hooks/ralph-loop.py +8 -1
  60. package/dist/templates/iflow/hooks/session-start.py +60 -19
  61. package/dist/templates/markdown/spec/backend/directory-structure.md +1 -1
  62. package/dist/templates/opencode/agents/dispatch.md +20 -19
  63. package/dist/templates/opencode/lib/trellis-context.js +35 -239
  64. package/dist/templates/opencode/plugins/inject-subagent-context.js +71 -121
  65. package/dist/templates/opencode/plugins/session-start.js +150 -146
  66. package/dist/templates/trellis/scripts/add_session.py +6 -1
  67. package/dist/templates/trellis/scripts/common/__init__.py +2 -0
  68. package/dist/templates/trellis/scripts/common/cli_adapter.py +87 -9
  69. package/dist/templates/trellis/scripts/common/paths.py +57 -6
  70. package/dist/templates/trellis/scripts/common/task_store.py +6 -4
  71. package/dist/templates/trellis/scripts/common/task_utils.py +14 -8
  72. package/dist/templates/trellis/scripts/multi_agent/start.py +9 -5
  73. package/dist/templates/trellis/scripts/task.py +1 -1
  74. package/dist/templates/trellis/workflow.md +17 -4
  75. package/dist/templates/windsurf/index.d.ts +21 -0
  76. package/dist/templates/windsurf/index.d.ts.map +1 -0
  77. package/dist/templates/windsurf/index.js +44 -0
  78. package/dist/templates/windsurf/index.js.map +1 -0
  79. package/dist/templates/windsurf/workflows/trellis-before-dev.md +31 -0
  80. package/dist/templates/windsurf/workflows/trellis-brainstorm.md +491 -0
  81. package/dist/templates/windsurf/workflows/trellis-break-loop.md +111 -0
  82. package/dist/templates/windsurf/workflows/trellis-check-cross-layer.md +157 -0
  83. package/dist/templates/windsurf/workflows/trellis-check.md +27 -0
  84. package/dist/templates/windsurf/workflows/trellis-create-command.md +154 -0
  85. package/dist/templates/windsurf/workflows/trellis-finish-work.md +147 -0
  86. package/dist/templates/windsurf/workflows/trellis-integrate-skill.md +220 -0
  87. package/dist/templates/windsurf/workflows/trellis-onboard.md +362 -0
  88. package/dist/templates/windsurf/workflows/trellis-record-session.md +66 -0
  89. package/dist/templates/windsurf/workflows/trellis-start.md +373 -0
  90. package/dist/templates/windsurf/workflows/trellis-update-spec.md +358 -0
  91. package/dist/types/ai-tools.d.ts +5 -3
  92. package/dist/types/ai-tools.d.ts.map +1 -1
  93. package/dist/types/ai-tools.js +21 -1
  94. package/dist/types/ai-tools.js.map +1 -1
  95. package/dist/utils/template-fetcher.d.ts +17 -4
  96. package/dist/utils/template-fetcher.d.ts.map +1 -1
  97. package/dist/utils/template-fetcher.js +94 -12
  98. package/dist/utils/template-fetcher.js.map +1 -1
  99. package/package.json +1 -1
@@ -1,7 +1,7 @@
1
1
  """
2
2
  CLI Adapter for Multi-Platform Support.
3
3
 
4
- Abstracts differences between Claude Code, OpenCode, Cursor, iFlow, Codex, Kilo, Kiro Code, Gemini CLI, Antigravity, Qoder, and CodeBuddy interfaces.
4
+ Abstracts differences between Claude Code, OpenCode, Cursor, iFlow, Codex, Kilo, Kiro Code, Gemini CLI, Antigravity, Windsurf, Qoder, CodeBuddy, and GitHub Copilot interfaces.
5
5
 
6
6
  Supported platforms:
7
7
  - claude: Claude Code (default)
@@ -13,8 +13,10 @@ Supported platforms:
13
13
  - kiro: Kiro Code (skills-based)
14
14
  - gemini: Gemini CLI
15
15
  - antigravity: Antigravity (workflow-based)
16
+ - windsurf: Windsurf (workflow-based)
16
17
  - qoder: Qoder
17
18
  - codebuddy: CodeBuddy
19
+ - copilot: GitHub Copilot (VS Code)
18
20
 
19
21
  Usage:
20
22
  from common.cli_adapter import CLIAdapter
@@ -43,8 +45,10 @@ Platform = Literal[
43
45
  "kiro",
44
46
  "gemini",
45
47
  "antigravity",
48
+ "windsurf",
46
49
  "qoder",
47
50
  "codebuddy",
51
+ "copilot",
48
52
  ]
49
53
 
50
54
 
@@ -89,7 +93,7 @@ class CLIAdapter:
89
93
  """Get platform-specific config directory name.
90
94
 
91
95
  Returns:
92
- Directory name ('.claude', '.opencode', '.cursor', '.iflow', '.codex', '.kilocode', '.kiro', '.gemini', '.agent', '.qoder', or '.codebuddy')
96
+ Directory name ('.claude', '.opencode', '.cursor', '.iflow', '.codex', '.kilocode', '.kiro', '.gemini', '.agent', '.windsurf', '.qoder', or '.codebuddy')
93
97
  """
94
98
  if self.platform == "opencode":
95
99
  return ".opencode"
@@ -107,10 +111,14 @@ class CLIAdapter:
107
111
  return ".gemini"
108
112
  elif self.platform == "antigravity":
109
113
  return ".agent"
114
+ elif self.platform == "windsurf":
115
+ return ".windsurf"
110
116
  elif self.platform == "qoder":
111
117
  return ".qoder"
112
118
  elif self.platform == "codebuddy":
113
119
  return ".codebuddy"
120
+ elif self.platform == "copilot":
121
+ return ".github/copilot"
114
122
  else:
115
123
  return ".claude"
116
124
 
@@ -121,7 +129,7 @@ class CLIAdapter:
121
129
  project_root: Project root directory
122
130
 
123
131
  Returns:
124
- Path to config directory (.claude, .opencode, .cursor, .iflow, .codex, .kilocode, .kiro, .gemini, .agent, .qoder, or .codebuddy)
132
+ Path to config directory (.claude, .opencode, .cursor, .iflow, .codex, .kilocode, .kiro, .gemini, .agent, .windsurf, .qoder, or .codebuddy)
125
133
  """
126
134
  return project_root / self.config_dir_name
127
135
 
@@ -153,8 +161,19 @@ class CLIAdapter:
153
161
  Note:
154
162
  Cursor uses prefix naming: .cursor/commands/trellis-<name>.md
155
163
  Antigravity uses workflow directory: .agent/workflows/<name>.md
164
+ Windsurf uses workflow directory: .windsurf/workflows/trellis-<name>.md
165
+ Copilot uses prompt files: .github/prompts/<name>.prompt.md
156
166
  Claude/OpenCode use subdirectory: .claude/commands/trellis/<name>.md
157
167
  """
168
+ if self.platform == "windsurf":
169
+ workflow_dir = self.get_config_dir(project_root) / "workflows"
170
+ if not parts:
171
+ return workflow_dir
172
+ if len(parts) >= 2 and parts[0] == "trellis":
173
+ filename = parts[-1]
174
+ return workflow_dir / f"trellis-{filename}"
175
+ return workflow_dir / Path(*parts)
176
+
158
177
  if self.platform in ("antigravity", "kilo"):
159
178
  workflow_dir = self.get_config_dir(project_root) / "workflows"
160
179
  if not parts:
@@ -164,6 +183,17 @@ class CLIAdapter:
164
183
  return workflow_dir / filename
165
184
  return workflow_dir / Path(*parts)
166
185
 
186
+ if self.platform == "copilot":
187
+ prompts_dir = project_root / ".github" / "prompts"
188
+ if not parts:
189
+ return prompts_dir
190
+ if len(parts) >= 2 and parts[0] == "trellis":
191
+ filename = parts[-1]
192
+ if filename.endswith(".md"):
193
+ filename = filename[:-3]
194
+ return prompts_dir / f"{filename}.prompt.md"
195
+ return prompts_dir / Path(*parts)
196
+
167
197
  if not parts:
168
198
  return self.get_config_dir(project_root) / "commands"
169
199
 
@@ -192,6 +222,7 @@ class CLIAdapter:
192
222
  Kiro: .kiro/skills/<name>/SKILL.md
193
223
  Gemini: .gemini/commands/trellis/<name>.toml
194
224
  Antigravity: .agent/workflows/<name>.md
225
+ Windsurf: .windsurf/workflows/trellis-<name>.md
195
226
  Others: .{platform}/commands/trellis/<name>.md
196
227
  """
197
228
  if self.platform == "cursor":
@@ -204,8 +235,12 @@ class CLIAdapter:
204
235
  return f".gemini/commands/trellis/{name}.toml"
205
236
  elif self.platform == "antigravity":
206
237
  return f".agent/workflows/{name}.md"
238
+ elif self.platform == "windsurf":
239
+ return f".windsurf/workflows/trellis-{name}.md"
207
240
  elif self.platform == "kilo":
208
241
  return f".kilocode/workflows/{name}.md"
242
+ elif self.platform == "copilot":
243
+ return f".github/prompts/{name}.prompt.md"
209
244
  else:
210
245
  return f"{self.config_dir_name}/commands/trellis/{name}.md"
211
246
 
@@ -231,10 +266,14 @@ class CLIAdapter:
231
266
  return {} # Gemini CLI doesn't have a non-interactive env var
232
267
  elif self.platform == "antigravity":
233
268
  return {}
269
+ elif self.platform == "windsurf":
270
+ return {}
234
271
  elif self.platform == "qoder":
235
272
  return {}
236
273
  elif self.platform == "codebuddy":
237
274
  return {}
275
+ elif self.platform == "copilot":
276
+ return {}
238
277
  else:
239
278
  return {"CLAUDE_NON_INTERACTIVE": "1"}
240
279
 
@@ -300,12 +339,20 @@ class CLIAdapter:
300
339
  raise ValueError(
301
340
  "Antigravity workflows are UI slash commands; CLI agent run is not supported."
302
341
  )
342
+ elif self.platform == "windsurf":
343
+ raise ValueError(
344
+ "Windsurf workflows are UI slash commands; CLI agent run is not supported."
345
+ )
303
346
  elif self.platform == "qoder":
304
347
  cmd = ["qodercli", "-p", prompt]
305
348
  elif self.platform == "codebuddy":
306
349
  raise ValueError(
307
350
  "CodeBuddy does not support non-interactive mode (no CLI agent)"
308
351
  )
352
+ elif self.platform == "copilot":
353
+ raise ValueError(
354
+ "GitHub Copilot is IDE-only; CLI agent run is not supported."
355
+ )
309
356
 
310
357
  else: # claude
311
358
  cmd = ["claude", "-p"]
@@ -352,12 +399,20 @@ class CLIAdapter:
352
399
  raise ValueError(
353
400
  "Antigravity workflows are UI slash commands; CLI resume is not supported."
354
401
  )
402
+ elif self.platform == "windsurf":
403
+ raise ValueError(
404
+ "Windsurf workflows are UI slash commands; CLI resume is not supported."
405
+ )
355
406
  elif self.platform == "qoder":
356
407
  return ["qodercli", "--resume", session_id]
357
408
  elif self.platform == "codebuddy":
358
409
  raise ValueError(
359
410
  "CodeBuddy does not support non-interactive mode (no CLI agent)"
360
411
  )
412
+ elif self.platform == "copilot":
413
+ raise ValueError(
414
+ "GitHub Copilot is IDE-only; CLI resume is not supported."
415
+ )
361
416
  else:
362
417
  return ["claude", "--resume", session_id]
363
418
 
@@ -420,10 +475,14 @@ class CLIAdapter:
420
475
  return "gemini"
421
476
  elif self.platform == "antigravity":
422
477
  return "agy"
478
+ elif self.platform == "windsurf":
479
+ return "windsurf"
423
480
  elif self.platform == "qoder":
424
481
  return "qodercli"
425
482
  elif self.platform == "codebuddy":
426
483
  return "codebuddy"
484
+ elif self.platform == "copilot":
485
+ return "copilot"
427
486
  else:
428
487
  return "claude"
429
488
 
@@ -488,7 +547,7 @@ def get_cli_adapter(platform: str = "claude") -> CLIAdapter:
488
547
  """Get CLI adapter for the specified platform.
489
548
 
490
549
  Args:
491
- platform: Platform name ('claude', 'opencode', 'cursor', 'iflow', 'codex', 'kilo', 'kiro', 'gemini', 'antigravity', 'qoder', or 'codebuddy')
550
+ platform: Platform name ('claude', 'opencode', 'cursor', 'iflow', 'codex', 'kilo', 'kiro', 'gemini', 'antigravity', 'windsurf', 'qoder', or 'codebuddy')
492
551
 
493
552
  Returns:
494
553
  CLIAdapter instance
@@ -506,11 +565,13 @@ def get_cli_adapter(platform: str = "claude") -> CLIAdapter:
506
565
  "kiro",
507
566
  "gemini",
508
567
  "antigravity",
568
+ "windsurf",
509
569
  "qoder",
510
570
  "codebuddy",
571
+ "copilot",
511
572
  ):
512
573
  raise ValueError(
513
- f"Unsupported platform: {platform} (must be 'claude', 'opencode', 'cursor', 'iflow', 'codex', 'kilo', 'kiro', 'gemini', 'antigravity', 'qoder', or 'codebuddy')"
574
+ f"Unsupported platform: {platform} (must be 'claude', 'opencode', 'cursor', 'iflow', 'codex', 'kilo', 'kiro', 'gemini', 'antigravity', 'windsurf', 'qoder', 'codebuddy', or 'copilot')"
514
575
  )
515
576
 
516
577
  return CLIAdapter(platform=platform) # type: ignore
@@ -527,8 +588,10 @@ _ALL_PLATFORM_CONFIG_DIRS = (
527
588
  ".kiro",
528
589
  ".gemini",
529
590
  ".agent",
591
+ ".windsurf",
530
592
  ".qoder",
531
593
  ".codebuddy",
594
+ ".github/copilot",
532
595
  )
533
596
  """All platform config directory names (used by detect_platform exclusion checks)."""
534
597
 
@@ -555,15 +618,16 @@ def detect_platform(project_root: Path) -> Platform:
555
618
  7. .kiro/skills exists and no other platform dirs → kiro
556
619
  8. .gemini directory exists → gemini
557
620
  9. .agent/workflows exists and no other platform dirs → antigravity
558
- 10. .codebuddy directory exists → codebuddy
559
- 11. .qoder directory exists → qoder
560
- 12. Defaultclaude
621
+ 10. .windsurf/workflows exists and no other platform dirs windsurf
622
+ 11. .codebuddy directory exists → codebuddy
623
+ 12. .qoder directory exists qoder
624
+ 13. Default → claude
561
625
 
562
626
  Args:
563
627
  project_root: Project root directory
564
628
 
565
629
  Returns:
566
- Detected platform ('claude', 'opencode', 'cursor', 'iflow', 'codex', 'kilo', 'kiro', 'gemini', 'antigravity', 'qoder', 'codebuddy', or default 'claude')
630
+ Detected platform ('claude', 'opencode', 'cursor', 'iflow', 'codex', 'kilo', 'kiro', 'gemini', 'antigravity', 'windsurf', 'qoder', 'codebuddy', or default 'claude')
567
631
  """
568
632
  import os
569
633
 
@@ -579,8 +643,10 @@ def detect_platform(project_root: Path) -> Platform:
579
643
  "kiro",
580
644
  "gemini",
581
645
  "antigravity",
646
+ "windsurf",
582
647
  "qoder",
583
648
  "codebuddy",
649
+ "copilot",
584
650
  ):
585
651
  return env_platform # type: ignore
586
652
 
@@ -626,6 +692,14 @@ def detect_platform(project_root: Path) -> Platform:
626
692
  ):
627
693
  return "antigravity"
628
694
 
695
+ # Check for Windsurf workflow directory only when no other platform config exists
696
+ if (
697
+ project_root / ".windsurf" / "workflows"
698
+ ).is_dir() and not _has_other_platform_dir(
699
+ project_root, {".windsurf"}
700
+ ):
701
+ return "windsurf"
702
+
629
703
  # Check for .codebuddy directory (CodeBuddy-specific)
630
704
  if (project_root / ".codebuddy").is_dir():
631
705
  return "codebuddy"
@@ -634,6 +708,10 @@ def detect_platform(project_root: Path) -> Platform:
634
708
  if (project_root / ".qoder").is_dir():
635
709
  return "qoder"
636
710
 
711
+ # Check for .github/copilot directory (GitHub Copilot-specific)
712
+ if (project_root / ".github" / "copilot").is_dir():
713
+ return "copilot"
714
+
637
715
  return "claude"
638
716
 
639
717
 
@@ -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
@@ -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
  # =============================================================================
@@ -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
 
@@ -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
 
@@ -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"}
@@ -0,0 +1,31 @@
1
+ ---
2
+ description: Read the relevant development guidelines before starting your task.
3
+ ---
4
+
5
+ Execute these steps:
6
+
7
+ 1. **Discover packages and their spec layers**:
8
+ ```bash
9
+ python3 ./.trellis/scripts/get_context.py --mode packages
10
+ ```
11
+
12
+ 2. **Identify which specs apply** to your task based on:
13
+ - Which package you're modifying (e.g., `cli/`, `docs-site/`)
14
+ - What type of work (backend, frontend, unit-test, docs, etc.)
15
+
16
+ 3. **Read the spec index** for each relevant module:
17
+ ```bash
18
+ cat .trellis/spec/<package>/<layer>/index.md
19
+ ```
20
+ Follow the **"Pre-Development Checklist"** section in the index.
21
+
22
+ 4. **Read the specific guideline files** listed in the Pre-Development Checklist that are relevant to your task. The index is NOT the goal — it points you to the actual guideline files (e.g., `error-handling.md`, `conventions.md`, `mock-strategies.md`). Read those files to understand the coding standards and patterns.
23
+
24
+ 5. **Always read shared guides**:
25
+ ```bash
26
+ cat .trellis/spec/guides/index.md
27
+ ```
28
+
29
+ 6. Understand the coding standards and patterns you need to follow, then proceed with your development plan.
30
+
31
+ This step is **mandatory** before writing any code.