@mindfoldhq/trellis 0.4.0-beta.8 → 0.4.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +165 -13
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +14 -2
- package/dist/commands/update.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.10.json +9 -0
- package/dist/migrations/manifests/0.4.0-beta.9.json +9 -0
- package/dist/migrations/manifests/0.4.0-rc.0.json +9 -0
- package/dist/templates/claude/hooks/inject-subagent-context.py +8 -1
- package/dist/templates/claude/hooks/ralph-loop.py +18 -10
- package/dist/templates/claude/hooks/session-start.py +60 -19
- package/dist/templates/claude/hooks/statusline.py +218 -0
- package/dist/templates/claude/settings.json +4 -0
- package/dist/templates/codex/hooks/session-start.py +60 -21
- package/dist/templates/codex/hooks.json +1 -1
- package/dist/templates/copilot/hooks/session-start.py +243 -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 +60 -19
- 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 +35 -239
- package/dist/templates/opencode/plugins/inject-subagent-context.js +71 -121
- package/dist/templates/opencode/plugins/session-start.js +150 -146
- 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/trellis/workflow.md +17 -4
- 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
|
@@ -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
|
|
|
@@ -196,21 +196,30 @@ python3 ./.trellis/scripts/task.py create "<title>" --slug <task-name>
|
|
|
196
196
|
1. Create or select task
|
|
197
197
|
--> python3 ./.trellis/scripts/task.py create "<title>" --slug <name> or list
|
|
198
198
|
|
|
199
|
-
2.
|
|
199
|
+
2. Start task (mark as current)
|
|
200
|
+
--> python3 ./.trellis/scripts/task.py start <name>
|
|
201
|
+
--> Writes .trellis/.current-task; future sessions see it in <current-state>
|
|
202
|
+
|
|
203
|
+
3. Write code according to guidelines
|
|
200
204
|
--> Read .trellis/spec/ docs relevant to your task
|
|
201
205
|
--> For cross-layer: read .trellis/spec/guides/
|
|
202
206
|
|
|
203
|
-
|
|
207
|
+
4. Self-test
|
|
204
208
|
--> Run project's lint/test commands (see spec docs)
|
|
205
209
|
--> Manual feature testing
|
|
206
210
|
|
|
207
|
-
|
|
211
|
+
5. Commit code
|
|
208
212
|
--> git add <files>
|
|
209
213
|
--> git commit -m "type(scope): description"
|
|
210
214
|
Format: feat/fix/docs/refactor/test/chore
|
|
211
215
|
|
|
212
|
-
|
|
216
|
+
6. Record session (one command)
|
|
213
217
|
--> python3 ./.trellis/scripts/add_session.py --title "Title" --commit "hash"
|
|
218
|
+
|
|
219
|
+
7. Finish task (clear current)
|
|
220
|
+
--> python3 ./.trellis/scripts/task.py finish
|
|
221
|
+
--> Only when the task is fully done; otherwise leave it set so the
|
|
222
|
+
next session resumes where you left off
|
|
214
223
|
```
|
|
215
224
|
|
|
216
225
|
### Code Quality Checklist
|
|
@@ -315,11 +324,15 @@ tasks/
|
|
|
315
324
|
**Commands**:
|
|
316
325
|
```bash
|
|
317
326
|
python3 ./.trellis/scripts/task.py create "<title>" [--slug <name>] # Create task directory
|
|
327
|
+
python3 ./.trellis/scripts/task.py start <name> # Set as current task (writes .current-task, triggers after_start hooks)
|
|
328
|
+
python3 ./.trellis/scripts/task.py finish # Clear current task (triggers after_finish hooks)
|
|
318
329
|
python3 ./.trellis/scripts/task.py archive <name> # Archive to archive/{year-month}/
|
|
319
330
|
python3 ./.trellis/scripts/task.py list # List active tasks
|
|
320
331
|
python3 ./.trellis/scripts/task.py list-archive # List archived tasks
|
|
321
332
|
```
|
|
322
333
|
|
|
334
|
+
**Current task mechanism**: `task.py start <name>` writes the selected task path to `.trellis/.current-task`. The SessionStart hook reads this file to inject `## CURRENT TASK` into every new session's context, so the AI immediately knows what you're working on without being told. Run `task.py finish` when you're done — subsequent sessions will show `(none)` until you start another task.
|
|
335
|
+
|
|
323
336
|
---
|
|
324
337
|
|
|
325
338
|
## Best Practices
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Windsurf workflow templates
|
|
3
|
+
*
|
|
4
|
+
* These are GENERIC templates for user projects.
|
|
5
|
+
* Do NOT use Trellis project's own .windsurf/ directory (which may be customized).
|
|
6
|
+
*
|
|
7
|
+
* Directory structure:
|
|
8
|
+
* windsurf/
|
|
9
|
+
* └── workflows/ # Workflow files
|
|
10
|
+
*/
|
|
11
|
+
export interface WorkflowTemplate {
|
|
12
|
+
name: string;
|
|
13
|
+
content: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Get all workflow templates.
|
|
17
|
+
* Workflow names match their filename stem
|
|
18
|
+
* (e.g. trellis-start.md -> /trellis-start).
|
|
19
|
+
*/
|
|
20
|
+
export declare function getAllWorkflows(): WorkflowTemplate[];
|
|
21
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/templates/windsurf/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAqBH,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,IAAI,gBAAgB,EAAE,CAcpD"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Windsurf workflow templates
|
|
3
|
+
*
|
|
4
|
+
* These are GENERIC templates for user projects.
|
|
5
|
+
* Do NOT use Trellis project's own .windsurf/ directory (which may be customized).
|
|
6
|
+
*
|
|
7
|
+
* Directory structure:
|
|
8
|
+
* windsurf/
|
|
9
|
+
* └── workflows/ # Workflow files
|
|
10
|
+
*/
|
|
11
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
12
|
+
import { dirname, join } from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = dirname(__filename);
|
|
16
|
+
function readTemplate(relativePath) {
|
|
17
|
+
return readFileSync(join(__dirname, relativePath), "utf-8");
|
|
18
|
+
}
|
|
19
|
+
function listFiles(dir) {
|
|
20
|
+
try {
|
|
21
|
+
return readdirSync(join(__dirname, dir));
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Get all workflow templates.
|
|
29
|
+
* Workflow names match their filename stem
|
|
30
|
+
* (e.g. trellis-start.md -> /trellis-start).
|
|
31
|
+
*/
|
|
32
|
+
export function getAllWorkflows() {
|
|
33
|
+
const workflows = [];
|
|
34
|
+
for (const file of listFiles("workflows")) {
|
|
35
|
+
if (!file.endsWith(".md")) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const name = file.replace(".md", "");
|
|
39
|
+
const content = readTemplate(`workflows/${file}`);
|
|
40
|
+
workflows.push({ name, content });
|
|
41
|
+
}
|
|
42
|
+
return workflows;
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/templates/windsurf/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAClD,MAAM,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;AAEtC,SAAS,YAAY,CAAC,YAAoB;IACxC,OAAO,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,YAAY,CAAC,EAAE,OAAO,CAAC,CAAC;AAC9D,CAAC;AAED,SAAS,SAAS,CAAC,GAAW;IAC5B,IAAI,CAAC;QACH,OAAO,WAAW,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC,CAAC;IAC3C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAOD;;;;GAIG;AACH,MAAM,UAAU,eAAe;IAC7B,MAAM,SAAS,GAAuB,EAAE,CAAC;IAEzC,KAAK,MAAM,IAAI,IAAI,SAAS,CAAC,WAAW,CAAC,EAAE,CAAC;QAC1C,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;YAC1B,SAAS;QACX,CAAC;QAED,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QACrC,MAAM,OAAO,GAAG,YAAY,CAAC,aAAa,IAAI,EAAE,CAAC,CAAC;QAClD,SAAS,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;IACpC,CAAC;IAED,OAAO,SAAS,CAAC;AACnB,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Read the relevant development guidelines before starting your task.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Execute these steps:
|
|
6
|
+
|
|
7
|
+
1. **Discover packages and their spec layers**:
|
|
8
|
+
```bash
|
|
9
|
+
python3 ./.trellis/scripts/get_context.py --mode packages
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
2. **Identify which specs apply** to your task based on:
|
|
13
|
+
- Which package you're modifying (e.g., `cli/`, `docs-site/`)
|
|
14
|
+
- What type of work (backend, frontend, unit-test, docs, etc.)
|
|
15
|
+
|
|
16
|
+
3. **Read the spec index** for each relevant module:
|
|
17
|
+
```bash
|
|
18
|
+
cat .trellis/spec/<package>/<layer>/index.md
|
|
19
|
+
```
|
|
20
|
+
Follow the **"Pre-Development Checklist"** section in the index.
|
|
21
|
+
|
|
22
|
+
4. **Read the specific guideline files** listed in the Pre-Development Checklist that are relevant to your task. The index is NOT the goal — it points you to the actual guideline files (e.g., `error-handling.md`, `conventions.md`, `mock-strategies.md`). Read those files to understand the coding standards and patterns.
|
|
23
|
+
|
|
24
|
+
5. **Always read shared guides**:
|
|
25
|
+
```bash
|
|
26
|
+
cat .trellis/spec/guides/index.md
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
6. Understand the coding standards and patterns you need to follow, then proceed with your development plan.
|
|
30
|
+
|
|
31
|
+
This step is **mandatory** before writing any code.
|