@mindfoldhq/trellis 0.4.0-beta.7 → 0.4.0-beta.9

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 (121) 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 +34 -10
  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 +33 -2
  10. package/dist/commands/update.js.map +1 -1
  11. package/dist/configurators/codex.d.ts +7 -4
  12. package/dist/configurators/codex.d.ts.map +1 -1
  13. package/dist/configurators/codex.js +40 -10
  14. package/dist/configurators/codex.js.map +1 -1
  15. package/dist/configurators/copilot.d.ts +9 -0
  16. package/dist/configurators/copilot.d.ts.map +1 -0
  17. package/dist/configurators/copilot.js +34 -0
  18. package/dist/configurators/copilot.js.map +1 -0
  19. package/dist/configurators/index.d.ts +11 -1
  20. package/dist/configurators/index.d.ts.map +1 -1
  21. package/dist/configurators/index.js +59 -4
  22. package/dist/configurators/index.js.map +1 -1
  23. package/dist/configurators/windsurf.d.ts +8 -0
  24. package/dist/configurators/windsurf.d.ts.map +1 -0
  25. package/dist/configurators/windsurf.js +18 -0
  26. package/dist/configurators/windsurf.js.map +1 -0
  27. package/dist/migrations/manifests/0.4.0-beta.8.json +34 -0
  28. package/dist/migrations/manifests/0.4.0-beta.9.json +9 -0
  29. package/dist/templates/claude/commands/trellis/record-session.md +2 -1
  30. package/dist/templates/claude/hooks/inject-subagent-context.py +8 -1
  31. package/dist/templates/claude/hooks/ralph-loop.py +8 -1
  32. package/dist/templates/claude/hooks/session-start.py +31 -7
  33. package/dist/templates/claude/hooks/statusline.py +211 -0
  34. package/dist/templates/claude/settings.json +4 -0
  35. package/dist/templates/codex/agents/check.toml +23 -0
  36. package/dist/templates/codex/agents/implement.toml +19 -0
  37. package/dist/templates/codex/agents/research.toml +26 -0
  38. package/dist/templates/codex/codex-skills/parallel/SKILL.md +194 -0
  39. package/dist/templates/codex/config.toml +5 -0
  40. package/dist/templates/codex/hooks/session-start.py +228 -0
  41. package/dist/templates/codex/hooks.json +16 -0
  42. package/dist/templates/codex/index.d.ts +27 -5
  43. package/dist/templates/codex/index.d.ts.map +1 -1
  44. package/dist/templates/codex/index.js +60 -8
  45. package/dist/templates/codex/index.js.map +1 -1
  46. package/dist/templates/codex/skills/improve-ut/SKILL.md +69 -0
  47. package/dist/templates/codex/skills/record-session/SKILL.md +2 -1
  48. package/dist/templates/copilot/hooks/session-start.py +218 -0
  49. package/dist/templates/copilot/hooks.json +11 -0
  50. package/dist/templates/copilot/index.d.ts +23 -0
  51. package/dist/templates/copilot/index.d.ts.map +1 -0
  52. package/dist/templates/copilot/index.js +54 -0
  53. package/dist/templates/copilot/index.js.map +1 -0
  54. package/dist/templates/copilot/prompts/before-dev.prompt.md +33 -0
  55. package/dist/templates/copilot/prompts/brainstorm.prompt.md +491 -0
  56. package/dist/templates/copilot/prompts/break-loop.prompt.md +129 -0
  57. package/dist/templates/copilot/prompts/check-cross-layer.prompt.md +157 -0
  58. package/dist/templates/copilot/prompts/check.prompt.md +29 -0
  59. package/dist/templates/copilot/prompts/create-command.prompt.md +116 -0
  60. package/dist/templates/copilot/prompts/finish-work.prompt.md +157 -0
  61. package/dist/templates/copilot/prompts/integrate-skill.prompt.md +223 -0
  62. package/dist/templates/copilot/prompts/onboard.prompt.md +362 -0
  63. package/dist/templates/copilot/prompts/parallel.prompt.md +196 -0
  64. package/dist/templates/copilot/prompts/record-session.prompt.md +66 -0
  65. package/dist/templates/copilot/prompts/start.prompt.md +397 -0
  66. package/dist/templates/copilot/prompts/update-spec.prompt.md +358 -0
  67. package/dist/templates/cursor/commands/trellis-record-session.md +2 -1
  68. package/dist/templates/extract.d.ts +29 -0
  69. package/dist/templates/extract.d.ts.map +1 -1
  70. package/dist/templates/extract.js +51 -0
  71. package/dist/templates/extract.js.map +1 -1
  72. package/dist/templates/gemini/commands/trellis/record-session.toml +2 -1
  73. package/dist/templates/iflow/commands/trellis/record-session.md +2 -1
  74. package/dist/templates/iflow/hooks/inject-subagent-context.py +8 -1
  75. package/dist/templates/iflow/hooks/ralph-loop.py +8 -1
  76. package/dist/templates/iflow/hooks/session-start.py +31 -7
  77. package/dist/templates/kilo/workflows/record-session.md +2 -1
  78. package/dist/templates/kiro/skills/record-session/SKILL.md +2 -1
  79. package/dist/templates/markdown/agents.md +4 -0
  80. package/dist/templates/markdown/spec/backend/directory-structure.md +1 -1
  81. package/dist/templates/markdown/workspace-index.md +2 -0
  82. package/dist/templates/opencode/agents/dispatch.md +20 -19
  83. package/dist/templates/opencode/commands/trellis/record-session.md +2 -1
  84. package/dist/templates/opencode/lib/trellis-context.js +42 -2
  85. package/dist/templates/opencode/plugins/session-start.js +7 -27
  86. package/dist/templates/qoder/skills/record-session/SKILL.md +2 -1
  87. package/dist/templates/trellis/scripts/add_session.py +69 -16
  88. package/dist/templates/trellis/scripts/common/__init__.py +2 -0
  89. package/dist/templates/trellis/scripts/common/cli_adapter.py +108 -16
  90. package/dist/templates/trellis/scripts/common/developer.py +2 -2
  91. package/dist/templates/trellis/scripts/common/paths.py +57 -6
  92. package/dist/templates/trellis/scripts/common/task_store.py +6 -4
  93. package/dist/templates/trellis/scripts/common/task_utils.py +14 -8
  94. package/dist/templates/trellis/scripts/multi_agent/plan.py +7 -6
  95. package/dist/templates/trellis/scripts/multi_agent/start.py +16 -11
  96. package/dist/templates/trellis/scripts/task.py +1 -1
  97. package/dist/templates/windsurf/index.d.ts +21 -0
  98. package/dist/templates/windsurf/index.d.ts.map +1 -0
  99. package/dist/templates/windsurf/index.js +44 -0
  100. package/dist/templates/windsurf/index.js.map +1 -0
  101. package/dist/templates/windsurf/workflows/trellis-before-dev.md +31 -0
  102. package/dist/templates/windsurf/workflows/trellis-brainstorm.md +491 -0
  103. package/dist/templates/windsurf/workflows/trellis-break-loop.md +111 -0
  104. package/dist/templates/windsurf/workflows/trellis-check-cross-layer.md +157 -0
  105. package/dist/templates/windsurf/workflows/trellis-check.md +27 -0
  106. package/dist/templates/windsurf/workflows/trellis-create-command.md +154 -0
  107. package/dist/templates/windsurf/workflows/trellis-finish-work.md +147 -0
  108. package/dist/templates/windsurf/workflows/trellis-integrate-skill.md +220 -0
  109. package/dist/templates/windsurf/workflows/trellis-onboard.md +362 -0
  110. package/dist/templates/windsurf/workflows/trellis-record-session.md +66 -0
  111. package/dist/templates/windsurf/workflows/trellis-start.md +373 -0
  112. package/dist/templates/windsurf/workflows/trellis-update-spec.md +358 -0
  113. package/dist/types/ai-tools.d.ts +15 -3
  114. package/dist/types/ai-tools.d.ts.map +1 -1
  115. package/dist/types/ai-tools.js +34 -2
  116. package/dist/types/ai-tools.js.map +1 -1
  117. package/dist/utils/template-fetcher.d.ts +17 -4
  118. package/dist/utils/template-fetcher.d.ts.map +1 -1
  119. package/dist/utils/template-fetcher.js +94 -12
  120. package/dist/utils/template-fetcher.js.map +1 -1
  121. 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', '.agents', '.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"
@@ -98,7 +102,7 @@ class CLIAdapter:
98
102
  elif self.platform == "iflow":
99
103
  return ".iflow"
100
104
  elif self.platform == "codex":
101
- return ".agents"
105
+ return ".codex"
102
106
  elif self.platform == "kilo":
103
107
  return ".kilocode"
104
108
  elif self.platform == "kiro":
@@ -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, .agents, .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
 
@@ -133,9 +141,11 @@ class CLIAdapter:
133
141
  project_root: Project root directory
134
142
 
135
143
  Returns:
136
- Path to agent .md file
144
+ Path to agent definition file (.md for most platforms, .toml for Codex)
137
145
  """
138
146
  mapped_name = self.get_agent_name(agent)
147
+ if self.platform == "codex":
148
+ return self.get_config_dir(project_root) / "agents" / f"{mapped_name}.toml"
139
149
  return self.get_config_dir(project_root) / "agents" / f"{mapped_name}.md"
140
150
 
141
151
  def get_commands_path(self, project_root: Path, *parts: str) -> Path:
@@ -151,8 +161,19 @@ class CLIAdapter:
151
161
  Note:
152
162
  Cursor uses prefix naming: .cursor/commands/trellis-<name>.md
153
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
154
166
  Claude/OpenCode use subdirectory: .claude/commands/trellis/<name>.md
155
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
+
156
177
  if self.platform in ("antigravity", "kilo"):
157
178
  workflow_dir = self.get_config_dir(project_root) / "workflows"
158
179
  if not parts:
@@ -162,6 +183,17 @@ class CLIAdapter:
162
183
  return workflow_dir / filename
163
184
  return workflow_dir / Path(*parts)
164
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
+
165
197
  if not parts:
166
198
  return self.get_config_dir(project_root) / "commands"
167
199
 
@@ -190,6 +222,7 @@ class CLIAdapter:
190
222
  Kiro: .kiro/skills/<name>/SKILL.md
191
223
  Gemini: .gemini/commands/trellis/<name>.toml
192
224
  Antigravity: .agent/workflows/<name>.md
225
+ Windsurf: .windsurf/workflows/trellis-<name>.md
193
226
  Others: .{platform}/commands/trellis/<name>.md
194
227
  """
195
228
  if self.platform == "cursor":
@@ -202,8 +235,12 @@ class CLIAdapter:
202
235
  return f".gemini/commands/trellis/{name}.toml"
203
236
  elif self.platform == "antigravity":
204
237
  return f".agent/workflows/{name}.md"
238
+ elif self.platform == "windsurf":
239
+ return f".windsurf/workflows/trellis-{name}.md"
205
240
  elif self.platform == "kilo":
206
241
  return f".kilocode/workflows/{name}.md"
242
+ elif self.platform == "copilot":
243
+ return f".github/prompts/{name}.prompt.md"
207
244
  else:
208
245
  return f"{self.config_dir_name}/commands/trellis/{name}.md"
209
246
 
@@ -229,10 +266,14 @@ class CLIAdapter:
229
266
  return {} # Gemini CLI doesn't have a non-interactive env var
230
267
  elif self.platform == "antigravity":
231
268
  return {}
269
+ elif self.platform == "windsurf":
270
+ return {}
232
271
  elif self.platform == "qoder":
233
272
  return {}
234
273
  elif self.platform == "codebuddy":
235
274
  return {}
275
+ elif self.platform == "copilot":
276
+ return {}
236
277
  else:
237
278
  return {"CLAUDE_NON_INTERACTIVE": "1"}
238
279
 
@@ -298,12 +339,20 @@ class CLIAdapter:
298
339
  raise ValueError(
299
340
  "Antigravity workflows are UI slash commands; CLI agent run is not supported."
300
341
  )
342
+ elif self.platform == "windsurf":
343
+ raise ValueError(
344
+ "Windsurf workflows are UI slash commands; CLI agent run is not supported."
345
+ )
301
346
  elif self.platform == "qoder":
302
347
  cmd = ["qodercli", "-p", prompt]
303
348
  elif self.platform == "codebuddy":
304
349
  raise ValueError(
305
350
  "CodeBuddy does not support non-interactive mode (no CLI agent)"
306
351
  )
352
+ elif self.platform == "copilot":
353
+ raise ValueError(
354
+ "GitHub Copilot is IDE-only; CLI agent run is not supported."
355
+ )
307
356
 
308
357
  else: # claude
309
358
  cmd = ["claude", "-p"]
@@ -350,12 +399,20 @@ class CLIAdapter:
350
399
  raise ValueError(
351
400
  "Antigravity workflows are UI slash commands; CLI resume is not supported."
352
401
  )
402
+ elif self.platform == "windsurf":
403
+ raise ValueError(
404
+ "Windsurf workflows are UI slash commands; CLI resume is not supported."
405
+ )
353
406
  elif self.platform == "qoder":
354
407
  return ["qodercli", "--resume", session_id]
355
408
  elif self.platform == "codebuddy":
356
409
  raise ValueError(
357
410
  "CodeBuddy does not support non-interactive mode (no CLI agent)"
358
411
  )
412
+ elif self.platform == "copilot":
413
+ raise ValueError(
414
+ "GitHub Copilot is IDE-only; CLI resume is not supported."
415
+ )
359
416
  else:
360
417
  return ["claude", "--resume", session_id]
361
418
 
@@ -418,10 +475,14 @@ class CLIAdapter:
418
475
  return "gemini"
419
476
  elif self.platform == "antigravity":
420
477
  return "agy"
478
+ elif self.platform == "windsurf":
479
+ return "windsurf"
421
480
  elif self.platform == "qoder":
422
481
  return "qodercli"
423
482
  elif self.platform == "codebuddy":
424
483
  return "codebuddy"
484
+ elif self.platform == "copilot":
485
+ return "copilot"
425
486
  else:
426
487
  return "claude"
427
488
 
@@ -429,9 +490,18 @@ class CLIAdapter:
429
490
  def supports_cli_agents(self) -> bool:
430
491
  """Check if platform supports running agents via CLI.
431
492
 
432
- Claude Code, OpenCode, and iFlow support CLI agent execution.
493
+ Claude Code, OpenCode, iFlow, and Codex support CLI agent execution.
433
494
  Cursor is IDE-only and doesn't support CLI agents.
434
495
  """
496
+ return self.platform in ("claude", "opencode", "iflow", "codex")
497
+
498
+ @property
499
+ def requires_agent_definition_file(self) -> bool:
500
+ """Check if platform requires an agent definition file (.md/.toml) to run.
501
+
502
+ Claude Code, OpenCode, iFlow: require agent .md files (--agent flag).
503
+ Codex: auto-discovers agents from .codex/agents/*.toml, no --agent flag.
504
+ """
435
505
  return self.platform in ("claude", "opencode", "iflow")
436
506
 
437
507
  # =========================================================================
@@ -477,7 +547,7 @@ def get_cli_adapter(platform: str = "claude") -> CLIAdapter:
477
547
  """Get CLI adapter for the specified platform.
478
548
 
479
549
  Args:
480
- 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')
481
551
 
482
552
  Returns:
483
553
  CLIAdapter instance
@@ -495,11 +565,13 @@ def get_cli_adapter(platform: str = "claude") -> CLIAdapter:
495
565
  "kiro",
496
566
  "gemini",
497
567
  "antigravity",
568
+ "windsurf",
498
569
  "qoder",
499
570
  "codebuddy",
571
+ "copilot",
500
572
  ):
501
573
  raise ValueError(
502
- 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')"
503
575
  )
504
576
 
505
577
  return CLIAdapter(platform=platform) # type: ignore
@@ -511,11 +583,15 @@ _ALL_PLATFORM_CONFIG_DIRS = (
511
583
  ".iflow",
512
584
  ".opencode",
513
585
  ".agents",
586
+ ".codex",
514
587
  ".kilocode",
515
588
  ".kiro",
516
589
  ".gemini",
517
590
  ".agent",
591
+ ".windsurf",
592
+ ".qoder",
518
593
  ".codebuddy",
594
+ ".github/copilot",
519
595
  )
520
596
  """All platform config directory names (used by detect_platform exclusion checks)."""
521
597
 
@@ -537,20 +613,21 @@ def detect_platform(project_root: Path) -> Platform:
537
613
  2. .opencode directory exists → opencode
538
614
  3. .iflow directory exists → iflow
539
615
  4. .cursor directory exists (without .claude) → cursor
540
- 5. .agents/skills exists and no other platform dirs → codex
616
+ 5. .codex exists and no other platform dirs → codex
541
617
  6. .kilocode directory exists → kilo
542
618
  7. .kiro/skills exists and no other platform dirs → kiro
543
619
  8. .gemini directory exists → gemini
544
620
  9. .agent/workflows exists and no other platform dirs → antigravity
545
- 10. .codebuddy directory exists → codebuddy
546
- 11. .qoder directory exists → qoder
547
- 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
548
625
 
549
626
  Args:
550
627
  project_root: Project root directory
551
628
 
552
629
  Returns:
553
- 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')
554
631
  """
555
632
  import os
556
633
 
@@ -566,8 +643,10 @@ def detect_platform(project_root: Path) -> Platform:
566
643
  "kiro",
567
644
  "gemini",
568
645
  "antigravity",
646
+ "windsurf",
569
647
  "qoder",
570
648
  "codebuddy",
649
+ "copilot",
571
650
  ):
572
651
  return env_platform # type: ignore
573
652
 
@@ -588,9 +667,10 @@ def detect_platform(project_root: Path) -> Platform:
588
667
  if (project_root / ".gemini").is_dir():
589
668
  return "gemini"
590
669
 
591
- # Check for Codex skills directory only when no other platform config exists
592
- if (project_root / ".agents" / "skills").is_dir() and not _has_other_platform_dir(
593
- project_root, {".agents"}
670
+ # Check for .codex directory (Codex-specific)
671
+ # .agents/skills/ alone does NOT trigger codex detection (it's a shared standard)
672
+ if (project_root / ".codex").is_dir() and not _has_other_platform_dir(
673
+ project_root, {".codex", ".agents"}
594
674
  ):
595
675
  return "codex"
596
676
 
@@ -612,6 +692,14 @@ def detect_platform(project_root: Path) -> Platform:
612
692
  ):
613
693
  return "antigravity"
614
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
+
615
703
  # Check for .codebuddy directory (CodeBuddy-specific)
616
704
  if (project_root / ".codebuddy").is_dir():
617
705
  return "codebuddy"
@@ -620,6 +708,10 @@ def detect_platform(project_root: Path) -> Platform:
620
708
  if (project_root / ".qoder").is_dir():
621
709
  return "qoder"
622
710
 
711
+ # Check for .github/copilot directory (GitHub Copilot-specific)
712
+ if (project_root / ".github" / "copilot").is_dir():
713
+ return "copilot"
714
+
623
715
  return "claude"
624
716
 
625
717
 
@@ -123,8 +123,8 @@ def init_developer(name: str, repo_root: Path | None = None) -> bool:
123
123
  ## Session History
124
124
 
125
125
  <!-- @@@auto:session-history -->
126
- | # | Date | Title | Commits |
127
- |---|------|-------|---------|
126
+ | # | Date | Title | Commits | Branch |
127
+ |---|------|-------|---------|--------|
128
128
  <!-- @@@/auto:session-history -->
129
129
 
130
130
  ---
@@ -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
  # =============================================================================
@@ -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
 
@@ -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"}