@mindfoldhq/trellis 0.4.0-beta.8 → 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 (92) 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 +33 -9
  7. package/dist/commands/init.js.map +1 -1
  8. package/dist/configurators/codex.d.ts.map +1 -1
  9. package/dist/configurators/codex.js +2 -1
  10. package/dist/configurators/codex.js.map +1 -1
  11. package/dist/configurators/copilot.d.ts +9 -0
  12. package/dist/configurators/copilot.d.ts.map +1 -0
  13. package/dist/configurators/copilot.js +34 -0
  14. package/dist/configurators/copilot.js.map +1 -0
  15. package/dist/configurators/index.d.ts.map +1 -1
  16. package/dist/configurators/index.js +32 -1
  17. package/dist/configurators/index.js.map +1 -1
  18. package/dist/configurators/windsurf.d.ts +8 -0
  19. package/dist/configurators/windsurf.d.ts.map +1 -0
  20. package/dist/configurators/windsurf.js +18 -0
  21. package/dist/configurators/windsurf.js.map +1 -0
  22. package/dist/migrations/manifests/0.4.0-beta.9.json +9 -0
  23. package/dist/templates/claude/hooks/inject-subagent-context.py +8 -1
  24. package/dist/templates/claude/hooks/ralph-loop.py +8 -1
  25. package/dist/templates/claude/hooks/session-start.py +31 -7
  26. package/dist/templates/claude/hooks/statusline.py +211 -0
  27. package/dist/templates/claude/settings.json +4 -0
  28. package/dist/templates/codex/hooks/session-start.py +31 -7
  29. package/dist/templates/codex/hooks.json +1 -1
  30. package/dist/templates/copilot/hooks/session-start.py +218 -0
  31. package/dist/templates/copilot/hooks.json +11 -0
  32. package/dist/templates/copilot/index.d.ts +23 -0
  33. package/dist/templates/copilot/index.d.ts.map +1 -0
  34. package/dist/templates/copilot/index.js +54 -0
  35. package/dist/templates/copilot/index.js.map +1 -0
  36. package/dist/templates/copilot/prompts/before-dev.prompt.md +33 -0
  37. package/dist/templates/copilot/prompts/brainstorm.prompt.md +491 -0
  38. package/dist/templates/copilot/prompts/break-loop.prompt.md +129 -0
  39. package/dist/templates/copilot/prompts/check-cross-layer.prompt.md +157 -0
  40. package/dist/templates/copilot/prompts/check.prompt.md +29 -0
  41. package/dist/templates/copilot/prompts/create-command.prompt.md +116 -0
  42. package/dist/templates/copilot/prompts/finish-work.prompt.md +157 -0
  43. package/dist/templates/copilot/prompts/integrate-skill.prompt.md +223 -0
  44. package/dist/templates/copilot/prompts/onboard.prompt.md +362 -0
  45. package/dist/templates/copilot/prompts/parallel.prompt.md +196 -0
  46. package/dist/templates/copilot/prompts/record-session.prompt.md +66 -0
  47. package/dist/templates/copilot/prompts/start.prompt.md +397 -0
  48. package/dist/templates/copilot/prompts/update-spec.prompt.md +358 -0
  49. package/dist/templates/extract.d.ts +18 -0
  50. package/dist/templates/extract.d.ts.map +1 -1
  51. package/dist/templates/extract.js +32 -0
  52. package/dist/templates/extract.js.map +1 -1
  53. package/dist/templates/iflow/hooks/inject-subagent-context.py +8 -1
  54. package/dist/templates/iflow/hooks/ralph-loop.py +8 -1
  55. package/dist/templates/iflow/hooks/session-start.py +31 -7
  56. package/dist/templates/markdown/spec/backend/directory-structure.md +1 -1
  57. package/dist/templates/opencode/agents/dispatch.md +20 -19
  58. package/dist/templates/opencode/lib/trellis-context.js +42 -2
  59. package/dist/templates/opencode/plugins/session-start.js +7 -27
  60. package/dist/templates/trellis/scripts/add_session.py +6 -1
  61. package/dist/templates/trellis/scripts/common/__init__.py +2 -0
  62. package/dist/templates/trellis/scripts/common/cli_adapter.py +87 -9
  63. package/dist/templates/trellis/scripts/common/paths.py +57 -6
  64. package/dist/templates/trellis/scripts/common/task_store.py +6 -4
  65. package/dist/templates/trellis/scripts/common/task_utils.py +14 -8
  66. package/dist/templates/trellis/scripts/multi_agent/start.py +9 -5
  67. package/dist/templates/trellis/scripts/task.py +1 -1
  68. package/dist/templates/windsurf/index.d.ts +21 -0
  69. package/dist/templates/windsurf/index.d.ts.map +1 -0
  70. package/dist/templates/windsurf/index.js +44 -0
  71. package/dist/templates/windsurf/index.js.map +1 -0
  72. package/dist/templates/windsurf/workflows/trellis-before-dev.md +31 -0
  73. package/dist/templates/windsurf/workflows/trellis-brainstorm.md +491 -0
  74. package/dist/templates/windsurf/workflows/trellis-break-loop.md +111 -0
  75. package/dist/templates/windsurf/workflows/trellis-check-cross-layer.md +157 -0
  76. package/dist/templates/windsurf/workflows/trellis-check.md +27 -0
  77. package/dist/templates/windsurf/workflows/trellis-create-command.md +154 -0
  78. package/dist/templates/windsurf/workflows/trellis-finish-work.md +147 -0
  79. package/dist/templates/windsurf/workflows/trellis-integrate-skill.md +220 -0
  80. package/dist/templates/windsurf/workflows/trellis-onboard.md +362 -0
  81. package/dist/templates/windsurf/workflows/trellis-record-session.md +66 -0
  82. package/dist/templates/windsurf/workflows/trellis-start.md +373 -0
  83. package/dist/templates/windsurf/workflows/trellis-update-spec.md +358 -0
  84. package/dist/types/ai-tools.d.ts +5 -3
  85. package/dist/types/ai-tools.d.ts.map +1 -1
  86. package/dist/types/ai-tools.js +21 -1
  87. package/dist/types/ai-tools.js.map +1 -1
  88. package/dist/utils/template-fetcher.d.ts +17 -4
  89. package/dist/utils/template-fetcher.d.ts.map +1 -1
  90. package/dist/utils/template-fetcher.js +94 -12
  91. package/dist/utils/template-fetcher.js.map +1 -1
  92. package/package.json +1 -1
@@ -11,7 +11,7 @@
11
11
  */
12
12
 
13
13
  import { existsSync, readFileSync, appendFileSync, readdirSync } from "fs"
14
- import { join } from "path"
14
+ import { isAbsolute, join } from "path"
15
15
  import { homedir, platform } from "os"
16
16
  import { execSync } from "child_process"
17
17
 
@@ -191,12 +191,52 @@ export class TrellisContext {
191
191
  if (!existsSync(currentTaskPath)) {
192
192
  return null
193
193
  }
194
- return readFileSync(currentTaskPath, "utf-8").trim()
194
+ const taskRef = readFileSync(currentTaskPath, "utf-8").trim()
195
+ const normalized = this.normalizeTaskRef(taskRef)
196
+ return normalized || null
195
197
  } catch {
196
198
  return null
197
199
  }
198
200
  }
199
201
 
202
+ normalizeTaskRef(taskRef) {
203
+ if (!taskRef) {
204
+ return ""
205
+ }
206
+
207
+ if (isAbsolute(taskRef)) {
208
+ return taskRef.trim()
209
+ }
210
+
211
+ let normalized = taskRef.trim().replace(/\\/g, "/")
212
+ while (normalized.startsWith("./")) {
213
+ normalized = normalized.slice(2)
214
+ }
215
+
216
+ if (normalized.startsWith("tasks/")) {
217
+ return `.trellis/${normalized}`
218
+ }
219
+
220
+ return normalized
221
+ }
222
+
223
+ resolveTaskDir(taskRef) {
224
+ const normalized = this.normalizeTaskRef(taskRef)
225
+ if (!normalized) {
226
+ return null
227
+ }
228
+
229
+ if (isAbsolute(normalized)) {
230
+ return normalized
231
+ }
232
+
233
+ if (normalized.startsWith(".trellis/")) {
234
+ return join(this.directory, normalized)
235
+ }
236
+
237
+ return join(this.directory, ".trellis", "tasks", normalized)
238
+ }
239
+
200
240
  // ============================================================
201
241
  // Hook Decision Logic
202
242
  // ============================================================
@@ -11,7 +11,7 @@
11
11
  */
12
12
 
13
13
  import { existsSync, readFileSync, readdirSync, statSync } from "fs"
14
- import { join } from "path"
14
+ import { basename, join } from "path"
15
15
  import { execFileSync } from "child_process"
16
16
  import { platform } from "os"
17
17
  import { TrellisContext, contextCollector, debugLog } from "../lib/trellis-context.js"
@@ -23,36 +23,16 @@ const PYTHON_CMD = platform() === "win32" ? "python" : "python3"
23
23
  * Check current task status and return structured status string.
24
24
  * JavaScript equivalent of _get_task_status in Claude's session-start.py.
25
25
  */
26
- function getTaskStatus(directory) {
27
- const trellisDir = join(directory, ".trellis")
28
- const currentTaskFile = join(trellisDir, ".current-task")
29
-
30
- if (!existsSync(currentTaskFile)) {
31
- return "Status: NO ACTIVE TASK\nNext: Describe what you want to work on"
32
- }
33
-
34
- let taskRef
35
- try {
36
- taskRef = readFileSync(currentTaskFile, "utf-8").trim()
37
- } catch {
38
- return "Status: NO ACTIVE TASK\nNext: Describe what you want to work on"
39
- }
40
-
26
+ function getTaskStatus(ctx) {
27
+ const taskRef = ctx.getCurrentTask()
41
28
  if (!taskRef) {
42
29
  return "Status: NO ACTIVE TASK\nNext: Describe what you want to work on"
43
30
  }
44
31
 
45
32
  // Resolve task directory
46
- let taskDir
47
- if (taskRef.startsWith("/")) {
48
- taskDir = taskRef
49
- } else if (taskRef.startsWith(".trellis/")) {
50
- taskDir = join(directory, taskRef)
51
- } else {
52
- taskDir = join(trellisDir, "tasks", taskRef)
53
- }
33
+ const taskDir = ctx.resolveTaskDir(taskRef)
54
34
 
55
- if (!existsSync(taskDir)) {
35
+ if (!taskDir || !existsSync(taskDir)) {
56
36
  return `Status: STALE POINTER\nTask: ${taskRef}\nNext: Task directory not found. Run: python3 ./.trellis/scripts/task.py finish`
57
37
  }
58
38
 
@@ -71,7 +51,7 @@ function getTaskStatus(directory) {
71
51
  const taskStatus = taskData.status || "unknown"
72
52
 
73
53
  if (taskStatus === "completed") {
74
- const dirName = taskDir.split("/").pop()
54
+ const dirName = basename(taskDir)
75
55
  return `Status: COMPLETED\nTask: ${taskTitle}\nNext: Archive with \`python3 ./.trellis/scripts/task.py archive ${dirName}\` or start a new task`
76
56
  }
77
57
 
@@ -354,7 +334,7 @@ Read and follow all instructions below carefully.
354
334
  }
355
335
 
356
336
  // 6. Task status (R2: check task state for session resume)
357
- const taskStatus = getTaskStatus(directory)
337
+ const taskStatus = getTaskStatus(ctx)
358
338
  parts.push(`<task-status>\n${taskStatus}\n</task-status>`)
359
339
 
360
340
  // 7. Final directive (R3: active, not passive)
@@ -316,11 +316,16 @@ def update_index(
316
316
  def _auto_commit_workspace(repo_root: Path) -> None:
317
317
  """Stage .trellis/workspace and .trellis/tasks, then commit with a configured message."""
318
318
  commit_msg = get_session_commit_message(repo_root)
319
- subprocess.run(
319
+ add_result = subprocess.run(
320
320
  ["git", "add", "-A", ".trellis/workspace", ".trellis/tasks"],
321
321
  cwd=repo_root,
322
322
  capture_output=True,
323
+ text=True,
323
324
  )
325
+ if add_result.returncode != 0:
326
+ print(f"[WARN] git add failed (exit {add_result.returncode}): {add_result.stderr.strip()}", file=sys.stderr)
327
+ print("[WARN] Please commit .trellis/ changes manually: git add .trellis && git commit", file=sys.stderr)
328
+ return
324
329
  # Check if there are staged changes
325
330
  result = subprocess.run(
326
331
  ["git", "diff", "--cached", "--quiet", "--", ".trellis/workspace", ".trellis/tasks"],
@@ -75,6 +75,8 @@ from .paths import (
75
75
  count_lines,
76
76
  get_current_task,
77
77
  get_current_task_abs,
78
+ normalize_task_ref,
79
+ resolve_task_ref,
78
80
  set_current_task,
79
81
  clear_current_task,
80
82
  has_current_task,
@@ -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
 
@@ -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"}