@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.
- package/README.md +10 -5
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +33 -9
- package/dist/commands/init.js.map +1 -1
- package/dist/configurators/codex.d.ts.map +1 -1
- package/dist/configurators/codex.js +2 -1
- package/dist/configurators/codex.js.map +1 -1
- package/dist/configurators/copilot.d.ts +9 -0
- package/dist/configurators/copilot.d.ts.map +1 -0
- package/dist/configurators/copilot.js +34 -0
- package/dist/configurators/copilot.js.map +1 -0
- package/dist/configurators/index.d.ts.map +1 -1
- package/dist/configurators/index.js +32 -1
- package/dist/configurators/index.js.map +1 -1
- package/dist/configurators/windsurf.d.ts +8 -0
- package/dist/configurators/windsurf.d.ts.map +1 -0
- package/dist/configurators/windsurf.js +18 -0
- package/dist/configurators/windsurf.js.map +1 -0
- package/dist/migrations/manifests/0.4.0-beta.9.json +9 -0
- package/dist/templates/claude/hooks/inject-subagent-context.py +8 -1
- package/dist/templates/claude/hooks/ralph-loop.py +8 -1
- package/dist/templates/claude/hooks/session-start.py +31 -7
- package/dist/templates/claude/hooks/statusline.py +211 -0
- package/dist/templates/claude/settings.json +4 -0
- package/dist/templates/codex/hooks/session-start.py +31 -7
- package/dist/templates/codex/hooks.json +1 -1
- package/dist/templates/copilot/hooks/session-start.py +218 -0
- package/dist/templates/copilot/hooks.json +11 -0
- package/dist/templates/copilot/index.d.ts +23 -0
- package/dist/templates/copilot/index.d.ts.map +1 -0
- package/dist/templates/copilot/index.js +54 -0
- package/dist/templates/copilot/index.js.map +1 -0
- package/dist/templates/copilot/prompts/before-dev.prompt.md +33 -0
- package/dist/templates/copilot/prompts/brainstorm.prompt.md +491 -0
- package/dist/templates/copilot/prompts/break-loop.prompt.md +129 -0
- package/dist/templates/copilot/prompts/check-cross-layer.prompt.md +157 -0
- package/dist/templates/copilot/prompts/check.prompt.md +29 -0
- package/dist/templates/copilot/prompts/create-command.prompt.md +116 -0
- package/dist/templates/copilot/prompts/finish-work.prompt.md +157 -0
- package/dist/templates/copilot/prompts/integrate-skill.prompt.md +223 -0
- package/dist/templates/copilot/prompts/onboard.prompt.md +362 -0
- package/dist/templates/copilot/prompts/parallel.prompt.md +196 -0
- package/dist/templates/copilot/prompts/record-session.prompt.md +66 -0
- package/dist/templates/copilot/prompts/start.prompt.md +397 -0
- package/dist/templates/copilot/prompts/update-spec.prompt.md +358 -0
- package/dist/templates/extract.d.ts +18 -0
- package/dist/templates/extract.d.ts.map +1 -1
- package/dist/templates/extract.js +32 -0
- package/dist/templates/extract.js.map +1 -1
- package/dist/templates/iflow/hooks/inject-subagent-context.py +8 -1
- package/dist/templates/iflow/hooks/ralph-loop.py +8 -1
- package/dist/templates/iflow/hooks/session-start.py +31 -7
- package/dist/templates/markdown/spec/backend/directory-structure.md +1 -1
- package/dist/templates/opencode/agents/dispatch.md +20 -19
- package/dist/templates/opencode/lib/trellis-context.js +42 -2
- package/dist/templates/opencode/plugins/session-start.js +7 -27
- package/dist/templates/trellis/scripts/add_session.py +6 -1
- package/dist/templates/trellis/scripts/common/__init__.py +2 -0
- package/dist/templates/trellis/scripts/common/cli_adapter.py +87 -9
- package/dist/templates/trellis/scripts/common/paths.py +57 -6
- package/dist/templates/trellis/scripts/common/task_store.py +6 -4
- package/dist/templates/trellis/scripts/common/task_utils.py +14 -8
- package/dist/templates/trellis/scripts/multi_agent/start.py +9 -5
- package/dist/templates/trellis/scripts/task.py +1 -1
- package/dist/templates/windsurf/index.d.ts +21 -0
- package/dist/templates/windsurf/index.d.ts.map +1 -0
- package/dist/templates/windsurf/index.js +44 -0
- package/dist/templates/windsurf/index.js.map +1 -0
- package/dist/templates/windsurf/workflows/trellis-before-dev.md +31 -0
- package/dist/templates/windsurf/workflows/trellis-brainstorm.md +491 -0
- package/dist/templates/windsurf/workflows/trellis-break-loop.md +111 -0
- package/dist/templates/windsurf/workflows/trellis-check-cross-layer.md +157 -0
- package/dist/templates/windsurf/workflows/trellis-check.md +27 -0
- package/dist/templates/windsurf/workflows/trellis-create-command.md +154 -0
- package/dist/templates/windsurf/workflows/trellis-finish-work.md +147 -0
- package/dist/templates/windsurf/workflows/trellis-integrate-skill.md +220 -0
- package/dist/templates/windsurf/workflows/trellis-onboard.md +362 -0
- package/dist/templates/windsurf/workflows/trellis-record-session.md +66 -0
- package/dist/templates/windsurf/workflows/trellis-start.md +373 -0
- package/dist/templates/windsurf/workflows/trellis-update-spec.md +358 -0
- package/dist/types/ai-tools.d.ts +5 -3
- package/dist/types/ai-tools.d.ts.map +1 -1
- package/dist/types/ai-tools.js +21 -1
- package/dist/types/ai-tools.js.map +1 -1
- package/dist/utils/template-fetcher.d.ts +17 -4
- package/dist/utils/template-fetcher.d.ts.map +1 -1
- package/dist/utils/template-fetcher.js +94 -12
- package/dist/utils/template-fetcher.js.map +1 -1
- 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
|
-
|
|
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(
|
|
27
|
-
const
|
|
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
|
-
|
|
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
|
|
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(
|
|
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"],
|
|
@@ -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
|
|
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 '
|
|
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. .
|
|
559
|
-
11. .
|
|
560
|
-
12.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
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": "
|
|
167
|
-
{"phase": 2, "action": "
|
|
168
|
-
{"phase": 3, "action": "
|
|
169
|
-
{"phase": 4, "action": "
|
|
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
|
|
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.
|
|
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
|
|
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 /
|
|
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.
|
|
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
|
|
196
|
-
return repo_root /
|
|
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 /
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
task_dir_abs =
|
|
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
|
-
|
|
210
|
-
|
|
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 =
|
|
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"}
|