@blxzer/cursor-trellis 0.2.8 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/CHANGELOG.md +89 -0
  2. package/README.md +37 -12
  3. package/README.zh-CN.md +41 -16
  4. package/bin/{trellis.js → cstl.js} +1 -1
  5. package/dist/cli/index.d.ts.map +1 -1
  6. package/dist/cli/index.js +30 -4
  7. package/dist/cli/index.js.map +1 -1
  8. package/dist/commands/capability-smoke.d.ts +24 -0
  9. package/dist/commands/capability-smoke.d.ts.map +1 -0
  10. package/dist/commands/capability-smoke.js +83 -0
  11. package/dist/commands/capability-smoke.js.map +1 -0
  12. package/dist/commands/init.d.ts.map +1 -1
  13. package/dist/commands/init.js +94 -34
  14. package/dist/commands/init.js.map +1 -1
  15. package/dist/commands/uninstall.d.ts +1 -1
  16. package/dist/commands/uninstall.js +2 -2
  17. package/dist/commands/uninstall.js.map +1 -1
  18. package/dist/commands/update.d.ts +1 -1
  19. package/dist/commands/update.d.ts.map +1 -1
  20. package/dist/commands/update.js +79 -34
  21. package/dist/commands/update.js.map +1 -1
  22. package/dist/commands/upgrade.js +2 -2
  23. package/dist/commands/upgrade.js.map +1 -1
  24. package/dist/configurators/cursor.d.ts +2 -2
  25. package/dist/configurators/cursor.js +4 -4
  26. package/dist/configurators/cursor.js.map +1 -1
  27. package/dist/configurators/index.js +3 -3
  28. package/dist/configurators/index.js.map +1 -1
  29. package/dist/configurators/shared.d.ts +10 -10
  30. package/dist/configurators/shared.d.ts.map +1 -1
  31. package/dist/configurators/shared.js +26 -26
  32. package/dist/configurators/shared.js.map +1 -1
  33. package/dist/configurators/workflow.d.ts +2 -2
  34. package/dist/migrations/manifests/0.2.10.json +374 -0
  35. package/dist/migrations/manifests/0.2.9.json +9 -0
  36. package/dist/migrations/manifests/0.3.0.json +89 -0
  37. package/dist/templates/common/bundled-skills/{trellis-cursor2plus-setup → cstl-cursor2plus-setup}/SKILL.md +5 -4
  38. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/SKILL.md +75 -75
  39. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/customize-local/add-project-local-conventions.md +8 -8
  40. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/customize-local/change-agents.md +6 -6
  41. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/customize-local/change-context-loading.md +7 -7
  42. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/customize-local/change-skills-or-commands.md +1 -1
  43. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/customize-local/change-spec-structure.md +2 -2
  44. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/customize-local/change-task-lifecycle.md +31 -31
  45. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/customize-local/change-workflow.md +12 -12
  46. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/customize-local/overview.md +55 -55
  47. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/local-architecture/context-injection.md +13 -13
  48. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/local-architecture/generated-files.md +83 -83
  49. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/local-architecture/overview.md +2 -2
  50. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/local-architecture/spec-system.md +1 -1
  51. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/local-architecture/task-system.md +22 -22
  52. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/local-architecture/workflow.md +5 -5
  53. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/local-architecture/workspace-memory.md +3 -3
  54. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/platform-files/agents.md +11 -11
  55. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/platform-files/hooks-and-settings.md +1 -1
  56. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/platform-files/overview.md +4 -4
  57. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/platform-files/platform-map.md +5 -5
  58. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/platform-files/skills-and-commands.md +15 -15
  59. package/dist/templates/common/bundled-skills/{trellis-micro-grill → cstl-micro-grill}/SKILL.md +62 -47
  60. package/dist/templates/common/bundled-skills/{trellis-skill-creator → cstl-skill-creator}/SKILL.md +44 -44
  61. package/dist/templates/common/bundled-skills/{trellis-skill-creator → cstl-skill-creator}/references/authoring-rules.md +97 -97
  62. package/dist/templates/common/bundled-skills/{trellis-skill-creator/references/trellis-skill-locations.md → cstl-skill-creator/references/cstl-skill-locations.md} +52 -52
  63. package/dist/templates/common/bundled-skills/{trellis-skill-creator → cstl-skill-creator}/references/review-checklist.md +47 -47
  64. package/dist/templates/common/bundled-skills/{trellis-spec-bootstrap → cstl-spec-bootstrap}/SKILL.md +1 -1
  65. package/dist/templates/common/bundled-skills/smart-search-cli/SKILL.md +7 -8
  66. package/dist/templates/common/bundled-skills/smart-search-cli/references/cli-contract.md +46 -46
  67. package/dist/templates/common/commands/continue.md +20 -20
  68. package/dist/templates/common/commands/start.md +11 -11
  69. package/dist/templates/common/index.d.ts +1 -1
  70. package/dist/templates/common/index.d.ts.map +1 -1
  71. package/dist/templates/common/skills/brainstorm.md +188 -164
  72. package/dist/templates/common/skills/check.md +1 -1
  73. package/dist/templates/cursor/agents/{trellis-check.md → cstl-check.md} +8 -8
  74. package/dist/templates/cursor/agents/{trellis-implement.md → cstl-implement.md} +8 -8
  75. package/dist/templates/cursor/agents/{trellis-research.md → cstl-research.md} +2 -2
  76. package/dist/templates/cursor/commands/cursor2plus-setup.md +5 -5
  77. package/dist/templates/cursor/fixtures/expected-rules.d.ts +1 -1
  78. package/dist/templates/cursor/fixtures/expected-rules.d.ts.map +1 -1
  79. package/dist/templates/cursor/fixtures/expected-rules.js +2 -2
  80. package/dist/templates/cursor/fixtures/expected-rules.js.map +1 -1
  81. package/dist/templates/cursor/rules/{trellis-subagent-dispatch.mdc → cstl-subagent-dispatch.mdc} +2 -2
  82. package/dist/templates/cursor/rules/{trellis-triage.mdc → cstl-triage.mdc} +1 -1
  83. package/dist/templates/markdown/agents.md +4 -4
  84. package/dist/templates/markdown/spec/guides/cross-platform-thinking-guide.md.txt +634 -634
  85. package/dist/templates/markdown/spec/guides/cursor-context-injection-guide.md.txt +4 -4
  86. package/dist/templates/markdown/spec/guides/cursor-semantic-compliance.md.txt +6 -0
  87. package/dist/templates/markdown/spec/guides/cursor-subagent-policy.md.txt +393 -391
  88. package/dist/templates/markdown/spec/guides/durable-learning-decision-guide.md.txt +6 -6
  89. package/dist/templates/markdown/spec/guides/execution-strategy.md.txt +2 -2
  90. package/dist/templates/markdown/spec/guides/index.md.txt +3 -2
  91. package/dist/templates/markdown/spec/guides/retrieval-daily-guide.md.txt +12 -0
  92. package/dist/templates/shared-hooks/index.d.ts +1 -1
  93. package/dist/templates/shared-hooks/index.js +1 -1
  94. package/dist/templates/shared-hooks/inject-subagent-context.py +3 -3
  95. package/dist/templates/shared-hooks/inject-workflow-state.py +27 -27
  96. package/dist/templates/shared-hooks/research-end-retrieval-pack.py +1 -1
  97. package/dist/templates/shared-hooks/session-start.py +3 -3
  98. package/dist/templates/trellis/config/execution-strategy-rules.json +1 -1
  99. package/dist/templates/trellis/config.yaml +4 -4
  100. package/dist/templates/trellis/local/README.md +12 -10
  101. package/dist/templates/trellis/local/patch_wpelc8.py +3 -3
  102. package/dist/templates/trellis/local/subagent-models.json.example +1 -1
  103. package/dist/templates/trellis/local/trellis-task-models.json.example +3 -3
  104. package/dist/templates/trellis/local/trellis-task-models.json5.example +5 -5
  105. package/dist/templates/trellis/local/trellis_task_models_config.py +6 -6
  106. package/dist/templates/trellis/scripts/common/cli_adapter.py +3 -3
  107. package/dist/templates/trellis/scripts/common/execution_strategy.py +1 -1
  108. package/dist/templates/trellis/scripts/common/parent_orchestration.py +702 -702
  109. package/dist/templates/trellis/scripts/common/session_context.py +1159 -1159
  110. package/dist/templates/trellis/scripts/common/smart_search_evidence.py +33 -1
  111. package/dist/templates/trellis/scripts/common/subagent_dispatch.py +5 -5
  112. package/dist/templates/trellis/scripts/common/task_gates.py +2056 -2034
  113. package/dist/templates/trellis/scripts/common/task_store.py +1594 -1594
  114. package/dist/templates/trellis/workflow.md +805 -805
  115. package/dist/types/ai-tools.d.ts +1 -1
  116. package/dist/types/ai-tools.d.ts.map +1 -1
  117. package/dist/types/ai-tools.js +1 -1
  118. package/dist/types/ai-tools.js.map +1 -1
  119. package/dist/utils/project-capabilities.d.ts +14 -3
  120. package/dist/utils/project-capabilities.d.ts.map +1 -1
  121. package/dist/utils/project-capabilities.js +122 -12
  122. package/dist/utils/project-capabilities.js.map +1 -1
  123. package/dist/utils/readiness.d.ts +10 -0
  124. package/dist/utils/readiness.d.ts.map +1 -1
  125. package/dist/utils/readiness.js +6 -3
  126. package/dist/utils/readiness.js.map +1 -1
  127. package/package.json +4 -5
  128. /package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/customize-local/change-hooks.md +0 -0
  129. /package/dist/templates/common/bundled-skills/{trellis-spec-bootstrap → cstl-spec-bootstrap}/references/mcp-setup.md +0 -0
  130. /package/dist/templates/common/bundled-skills/{trellis-spec-bootstrap → cstl-spec-bootstrap}/references/repository-analysis.md +0 -0
  131. /package/dist/templates/common/bundled-skills/{trellis-spec-bootstrap → cstl-spec-bootstrap}/references/spec-task-planning.md +0 -0
  132. /package/dist/templates/common/bundled-skills/{trellis-spec-bootstrap → cstl-spec-bootstrap}/references/spec-writing.md +0 -0
@@ -1,634 +1,634 @@
1
- # Cross-Platform Thinking Guide (OS / path)
2
-
3
- > **Purpose**: Catch OS and path assumptions before they become bugs.
4
- >
5
- > **Note (cursor-only fork):** This guide covers **operating-system and filesystem** portability (Windows vs macOS/Linux). AI platform adapter sync is Cursor-only; see `cross-layer-thinking-guide.md` for template consistency.
6
-
7
- ---
8
-
9
- ## Why This Matters
10
-
11
- **Most cross-platform bugs come from implicit assumptions**:
12
-
13
- - Assumed shebang works → breaks on Windows
14
- - Assumed `/` path separator → breaks on Windows
15
- - Assumed `\n` line endings → inconsistent behavior
16
- - Assumed command availability → `grep` vs `findstr`
17
-
18
- ---
19
-
20
- ## Platform Differences Checklist
21
-
22
- ### 1. Script Execution
23
-
24
- | Assumption | macOS/Linux | Windows |
25
- |------------|-------------|---------|
26
- | Shebang (`#!/usr/bin/env python3`) | ✅ Works | ❌ Ignored |
27
- | Direct execution (`./script.py`) | ✅ Works | ❌ Fails |
28
- | `python3` command | ✅ Always available | ⚠️ May need `python` |
29
- | `python` command | ⚠️ May be Python 2 | ✅ Usually Python 3 |
30
-
31
- **Rule 1**: For user-facing docs, help text, and error messages, either:
32
-
33
- - state the platform rule explicitly (`python` on Windows, `python3` elsewhere), or
34
- - render the command through the same platform-aware helper / placeholder the code uses.
35
-
36
- ```python
37
- # BAD - Assumes shebang works
38
- print("Usage: ./script.py <args>")
39
- print("Run: script.py <args>")
40
-
41
- # GOOD - Platform-aware wording
42
- print("Usage: python on Windows, python3 elsewhere")
43
- print("Run: {{PYTHON_CMD}} ./.trellis/scripts/task.py <args>")
44
- ```
45
-
46
- **Rule 2**: When generating config files at init time, use placeholder + platform detection:
47
-
48
- ```typescript
49
- // In template file (settings.json):
50
- { "command": "{{PYTHON_CMD}} .cursor/hooks/script.py" }
51
-
52
- // In configurator:
53
- function getPythonCommand(): string {
54
- return process.platform === "win32" ? "python" : "python3";
55
- }
56
-
57
- function replacePlaceholders(content: string): string {
58
- return content.replace(/\{\{PYTHON_CMD\}\}/g, getPythonCommand());
59
- }
60
- ```
61
-
62
- **Rule 3**: When calling Python at runtime from JavaScript, detect platform dynamically:
63
-
64
- ```javascript
65
- import { platform } from "os"
66
-
67
- const PYTHON_CMD = platform() === "win32" ? "python" : "python3"
68
- execSync(`${PYTHON_CMD} "${scriptPath}"`, { ... })
69
- ```
70
-
71
- **Rule 4**: If you need to verify Python is actually installed (not just choose
72
- the command), probe the same platform-selected alias you will later render or
73
- execute:
74
-
75
- ```typescript
76
- function getPythonCommand(platform = process.platform): string {
77
- return platform === "win32" ? "python" : "python3";
78
- }
79
-
80
- function warnIfPythonTooOld(): void {
81
- const cmd = getPythonCommand();
82
- try {
83
- execSync(`${cmd} --version`, { stdio: "pipe" });
84
- } catch {
85
- // Missing Python is a separate error path; don't silently swap aliases.
86
- }
87
- }
88
- ```
89
-
90
- **Rule 5**: Don't assume the Python version the AI CLI uses matches your shell's `python3`. The user's terminal may resolve `python3` → homebrew 3.11, but Cursor spawns hook subprocesses with a minimal PATH that resolves `python3` → `/usr/bin/python3` → macOS system 3.9. Distributed templates must either target the lowest plausible version or use `from __future__ import annotations` for PEP 604 syntax. See `cli/backend/script-conventions.md` → **CRITICAL: PEP 604 Annotations Require `from __future__ import annotations`** for the hard rule and audit check.
91
-
92
- **Rule 6**: When calling Python from Python, use `sys.executable`:
93
-
94
- ```python
95
- import sys
96
- import subprocess
97
-
98
- # BAD - Hardcoded command
99
- subprocess.run(["python3", "other_script.py"])
100
-
101
- # GOOD - Use current interpreter
102
- subprocess.run([sys.executable, "other_script.py"])
103
- ```
104
-
105
- ### 2. Path Handling
106
-
107
- | Assumption | macOS/Linux | Windows |
108
- |------------|-------------|---------|
109
- | `/` separator | ✅ Works | ⚠️ Sometimes works |
110
- | `\` separator | ❌ Escape char | ✅ Native |
111
- | `pathlib.Path` | ✅ Works | ✅ Works |
112
-
113
- **Rule (Python)**: Use `pathlib.Path` for all path operations.
114
-
115
- ```python
116
- # BAD - String concatenation
117
- path = base + "/" + filename
118
-
119
- # GOOD - pathlib
120
- from pathlib import Path
121
- path = Path(base) / filename
122
- ```
123
-
124
- #### Logical key vs filesystem path (TypeScript)
125
-
126
- A path string has two distinct roles. **Treat them differently.**
127
-
128
- | Role | OS-native (`\` on Windows) | Always POSIX (`/`) |
129
- |------|---------------------------|--------------------|
130
- | `fs.readFileSync(p)` / `path.join(cwd, x)` for fs call | ✅ Required | ❌ May fail on Windows |
131
- | `Map<relPath, content>` key, JSON field, hash dictionary key, anything persisted across OS | ❌ Cross-OS mismatch | ✅ Required |
132
-
133
- **Rule**: Anywhere a path string crosses OS or persists (Map keys consumed by another OS, JSON fields, hash dictionary keys), normalize to POSIX. Anywhere it goes straight to `fs.*`, leave OS-native.
134
-
135
- **Single source of truth**: `packages/cli/src/utils/posix.ts` exports `toPosix(p)`. Don't sprinkle `replaceAll('\\', '/')` at every `path.join` site — apply `toPosix` **once at the boundary**: collector exit (Map key entering hash dictionary) or write-time (`saveHashes` before `JSON.stringify`).
136
-
137
- ```typescript
138
- // BAD - logical key carries OS-native separator
139
- function collectTemplates(): Map<string, string> {
140
- const files = new Map<string, string>();
141
- for (const entry of walk(dir)) {
142
- files.set(path.join(".cursor", entry), readFile(entry)); // \ on Windows
143
- }
144
- return files;
145
- }
146
-
147
- // GOOD - normalize at the boundary
148
- import { toPosix } from "../utils/posix.js";
149
-
150
- function collectTemplates(): Map<string, string> {
151
- const files = new Map<string, string>();
152
- for (const entry of walk(dir)) {
153
- files.set(toPosix(path.join(".cursor", entry)), readFile(entry));
154
- }
155
- return files;
156
- }
157
-
158
- // ALSO ACCEPTABLE - write-side defense (for storage helpers like saveHashes)
159
- function saveHashes(cwd: string, hashes: Record<string, string>): void {
160
- const normalized = Object.fromEntries(
161
- Object.entries(hashes).map(([k, v]) => [toPosix(k), v])
162
- );
163
- fs.writeFileSync(getHashesPath(cwd), JSON.stringify(normalized, null, 2));
164
- }
165
- ```
166
-
167
- **Common offender**: `path.relative(cwd, fullPath)` produces `\` on Windows. If you then use that string as a hash dictionary lookup key (`hashes[relPath]`), `toPosix` it first, or the lookup misses on Windows.
168
-
169
- ### 3. Line Endings
170
-
171
- | Format | macOS/Linux | Windows | Git |
172
- |--------|-------------|---------|-----|
173
- | `\n` (LF) | ✅ Native | ⚠️ Some tools | ✅ Normalized |
174
- | `\r\n` (CRLF) | ⚠️ Extra char | ✅ Native | Converted |
175
-
176
- **Rule 1**: Use `.gitattributes` to enforce consistent line endings.
177
-
178
- ```gitattributes
179
- * text=auto eol=lf
180
- *.sh text eol=lf
181
- *.py text eol=lf
182
- ```
183
-
184
- **Rule 2**: When hashing or comparing **content** across platforms, normalize line endings before computing the hash. `.gitattributes` only governs git checkout — files written by users, scripts, or `core.autocrlf=true` may still arrive as CRLF, and `sha256(LF)` ≠ `sha256(CRLF)` for otherwise-identical content.
185
-
186
- ```typescript
187
- // BAD - Windows users with autocrlf=true get a different hash
188
- export function computeHash(content: string): string {
189
- return createHash("sha256").update(content, "utf-8").digest("hex");
190
- }
191
-
192
- // GOOD - normalize before hashing so logical content hashes identically
193
- export function computeHash(content: string): string {
194
- const normalized = content.replace(/\r\n/g, "\n");
195
- return createHash("sha256").update(normalized, "utf-8").digest("hex");
196
- }
197
- ```
198
-
199
- Apply this rule wherever the hash crosses OS boundaries (template hash dictionary, content fingerprints stored in JSON, integrity checks against a remote registry).
200
-
201
- ### 4. Environment Variables
202
-
203
- | Variable | macOS/Linux | Windows |
204
- |----------|-------------|---------|
205
- | `HOME` | ✅ Set | ❌ Use `USERPROFILE` |
206
- | `PATH` separator | `:` | `;` |
207
- | Case sensitivity | ✅ Case-sensitive | ❌ Case-insensitive |
208
-
209
- **Rule 1**: Use `pathlib.Path.home()` instead of environment variables.
210
-
211
- ```python
212
- # BAD
213
- home = os.environ.get("HOME")
214
-
215
- # GOOD
216
- home = Path.home()
217
- ```
218
-
219
- **Rule 2**: When injecting environment variables into shell commands, generate
220
- the prefix for the actual shell that will parse the command. Do not choose
221
- syntax from OS alone. AI tool "Bash" surfaces on Windows may execute through
222
- PowerShell, Git Bash, MSYS2, or another POSIX-like shell.
223
-
224
- ```javascript
225
- // BAD - breaks when the host shell is PowerShell
226
- command = `export TRELLIS_CONTEXT_ID=${shellQuote(contextKey)}; ${command}`;
227
-
228
- // GOOD - shell-dialect-aware command prefix
229
- const prefix = process.platform === "win32" && !isWindowsPosixShell(process.env)
230
- ? `$env:TRELLIS_CONTEXT_ID = ${powershellQuote(contextKey)}; `
231
- : `export TRELLIS_CONTEXT_ID=${shellQuote(contextKey)}; `;
232
- command = `${prefix}${command}`;
233
- ```
234
-
235
- On Windows, treat `MSYSTEM`, `MINGW_PREFIX`, `OSTYPE=msys|mingw|cygwin`,
236
- `SHELL=...bash`, or a platform-specific Git Bash setting as POSIX-shell
237
- signals. Keep PowerShell as the Windows default when there is no POSIX-shell
238
- signal.
239
-
240
- Also make duplicate-injection detection shell-aware. A guard that only matches
241
- `export VAR=` will miss PowerShell's `$env:VAR = ...` form and can wrap an
242
- already-correct command a second time.
243
-
244
- ### 5. Command Availability
245
-
246
- | Command | macOS/Linux | Windows |
247
- |---------|-------------|---------|
248
- | `grep` | ✅ Built-in | ❌ Not available |
249
- | `find` | ✅ Built-in | ⚠️ Different syntax |
250
- | `cat` | ✅ Built-in | ❌ Use `type` |
251
- | `tail -f` | ✅ Built-in | ❌ Not available |
252
-
253
- **Rule**: Use Python standard library instead of shell commands when possible.
254
-
255
- ```python
256
- # BAD - tail -f is not available on Windows
257
- subprocess.run(["tail", "-f", log_file])
258
-
259
- # GOOD - Cross-platform implementation
260
- def tail_follow(file_path: Path) -> None:
261
- """Follow a file like 'tail -f', cross-platform compatible."""
262
- with open(file_path, "r", encoding="utf-8", errors="replace") as f:
263
- f.seek(0, 2) # Go to end
264
- while True:
265
- line = f.readline()
266
- if line:
267
- print(line, end="", flush=True)
268
- else:
269
- time.sleep(0.1)
270
- ```
271
-
272
- ### Optional Advisory Checks in Agent Sandboxes
273
-
274
- AI CLI subprocesses may run with outbound network disabled even when the user's
275
- normal terminal has network access. Prefer local CLI probes over optional
276
- network probes when the local CLI already exposes the needed information.
277
-
278
- **Rule 1**: Do not let a failed optional advisory check consume a once-per-session
279
- marker. Write the marker only after the script resolves a usable value and can
280
- make the intended decision. Otherwise a transient sandbox/network failure hides
281
- the hint for the rest of the session.
282
-
283
- **Rule 2**: If a local command can provide the needed value, try it with a short
284
- timeout and captured output. For example, `trellis --version` already runs the
285
- CLI's version comparison logic and can support an actionable update prompt
286
- without duplicating npm registry parsing.
287
-
288
- **Rule 3**: Keep advisory checks silent on failure. The user-facing context output
289
- must not fail or become noisy because an advisory check could not complete.
290
-
291
- ### 6. File Encoding
292
-
293
- | Default Encoding | macOS/Linux | Windows |
294
- |------------------|-------------|---------|
295
- | Terminal | UTF-8 | Often CP1252 or GBK |
296
- | File I/O | UTF-8 | System locale |
297
- | Git output | UTF-8 | May vary |
298
-
299
- **Rule**: Always explicitly specify `encoding="utf-8"` and use `errors="replace"`.
300
-
301
- > **Checklist**: When writing scripts that print non-ASCII, did you configure stdout encoding?
302
- > See `backend/script-conventions.md` for the specific pattern.
303
-
304
- ```python
305
- # BAD - Relies on system default
306
- with open(file, "r") as f:
307
- content = f.read()
308
-
309
- result = subprocess.run(cmd, capture_output=True, text=True)
310
-
311
- # GOOD - Explicit encoding with error handling
312
- with open(file, "r", encoding="utf-8", errors="replace") as f:
313
- content = f.read()
314
-
315
- result = subprocess.run(
316
- cmd,
317
- capture_output=True,
318
- text=True,
319
- encoding="utf-8",
320
- errors="replace"
321
- )
322
- ```
323
-
324
- **Git commands**: Force UTF-8 output encoding:
325
-
326
- ```python
327
- # Force git to output UTF-8
328
- git_args = ["git", "-c", "i18n.logOutputEncoding=UTF-8"] + args
329
- result = subprocess.run(
330
- git_args,
331
- capture_output=True,
332
- text=True,
333
- encoding="utf-8",
334
- errors="replace"
335
- )
336
- ```
337
-
338
- ---
339
-
340
- ## Change Propagation Checklist
341
-
342
- When making platform-related changes, check **all these locations**:
343
-
344
- ### Commands / Skills Sync
345
- - [ ] New command/skill added to the Cursor templates (`src/templates/cursor/` and `src/templates/common/`)
346
- - [ ] Cursor test file updated with new entry in `EXPECTED_COMMAND_NAMES` / `EXPECTED_SKILL_NAMES`
347
- - [ ] Command format matches Cursor convention (see `platform-integration.md` → Command Format)
348
- - [ ] If the command exists in the dogfooded copy (`.cursor/`), sync it too
349
-
350
- ### Documentation & Help Text
351
- - [ ] Docstrings at top of Python files
352
- - [ ] `--help` output / argparse descriptions
353
- - [ ] Usage examples in README
354
- - [ ] Error messages that suggest commands
355
- - [ ] Markdown documentation (`.md` files)
356
-
357
- ### Code Locations
358
- - [ ] `src/templates/` - Template files for new projects
359
- - [ ] `.trellis/scripts/` - Project's own scripts (if self-hosting)
360
- - [ ] `dist/` - Built output (rebuild after changes)
361
-
362
- ### Search Pattern
363
- ```bash
364
- # Find all places that might need updating
365
- grep -r "python [a-z]" --include="*.py" --include="*.md"
366
- grep -r "{{PYTHON_CMD}}\\|python3\\|python " --include="*.py" --include="*.md"
367
- ```
368
-
369
- ---
370
-
371
- ## Pre-Commit Checklist
372
-
373
- Before committing cross-platform code:
374
-
375
- - [ ] User-facing Python invocations are platform-aware (`python` on Windows, `python3` elsewhere) or use `{{PYTHON_CMD}}`
376
- - [ ] Python subprocesses from Python use `sys.executable`
377
- - [ ] All paths use `pathlib.Path`
378
- - [ ] No hardcoded path separators (`/` or `\`)
379
- - [ ] Path strings used as logical/persisted keys (Map keys, JSON fields, hash dictionary keys) are normalized via `toPosix()`; `fs.*` calls keep OS-native paths
380
- - [ ] Content hashes computed across OSes normalize line endings (`\r\n` → `\n`) before hashing
381
- - [ ] Cross-OS JSON with potential legacy pollution carries a `__version` sentinel and the loader discards unknown/legacy versions
382
- - [ ] No platform-specific commands without fallbacks (e.g., `tail -f`)
383
- - [ ] Optional advisory checks do not burn once-per-session markers on failure
384
- - [ ] All file I/O specifies `encoding="utf-8"` and `errors="replace"`
385
- - [ ] All subprocess calls specify `encoding="utf-8"` and `errors="replace"`
386
- - [ ] Git commands use `-c i18n.logOutputEncoding=UTF-8`
387
- - [ ] External tool API formats verified from documentation
388
- - [ ] Documentation matches code behavior
389
- - [ ] Ran search to find all affected locations
390
-
391
- ### 7. External Tool API Contracts
392
-
393
- When integrating with external tools (Cursor, etc.), their API contracts are **implicit assumptions**.
394
-
395
- **Rule**: Verify API formats from official documentation, don't guess.
396
-
397
- ```python
398
- # BAD - Guessed format
399
- output = {"continue": True, "message": "..."}
400
-
401
- # GOOD - Verified format from documentation
402
- output = {
403
- "hookSpecificOutput": {
404
- "hookEventName": "SessionStart",
405
- "additionalContext": "..."
406
- }
407
- }
408
- ```
409
-
410
- > **Warning**: Different hook types may have different output formats.
411
- > Always check the specific documentation for each hook event.
412
-
413
- ---
414
-
415
- ## Cross-Platform Persisted JSON: Schema Migration Sentinel
416
-
417
- When a JSON file may be read/written across OSes (committed to git, synced via cloud, copied between machines) **and an older format may already exist on user disks with cross-platform pollution** (Windows-style keys, CRLF-derived hashes, locale-encoded strings), add a `__version` sentinel and let the loader discard old formats so the writer regenerates clean data.
418
-
419
- **Why not migrate-in-place?** Path-key migration (`\\` → `/`) plus hash-input migration (CRLF → LF re-hash) plus encoding fixes are correlated — trying to translate the old payload risks producing wrong values. Discarding and regenerating is **safe**: the data is recomputable from disk, and `loadX` returning `{}` triggers the existing init/update path to rebuild canonical entries.
420
-
421
- ```typescript
422
- const SCHEMA_VERSION = 2;
423
- type StoredV2 = { __version: number; hashes: Record<string, string> };
424
-
425
- export function loadHashes(cwd: string): Record<string, string> {
426
- const file = getHashesPath(cwd);
427
- if (!fs.existsSync(file)) return {};
428
-
429
- try {
430
- const parsed = JSON.parse(fs.readFileSync(file, "utf-8")) as unknown;
431
-
432
- // Reject legacy flat format (no __version) and unknown versions.
433
- // The next saveHashes / initializeHashes will write a fresh v2 file.
434
- if (
435
- !parsed ||
436
- typeof parsed !== "object" ||
437
- (parsed as StoredV2).__version !== SCHEMA_VERSION ||
438
- typeof (parsed as StoredV2).hashes !== "object"
439
- ) {
440
- return {};
441
- }
442
- return (parsed as StoredV2).hashes;
443
- } catch {
444
- return {};
445
- }
446
- }
447
-
448
- export function saveHashes(cwd: string, hashes: Record<string, string>): void {
449
- const payload: StoredV2 = { __version: SCHEMA_VERSION, hashes };
450
- fs.writeFileSync(getHashesPath(cwd), JSON.stringify(payload, null, 2));
451
- }
452
- ```
453
-
454
- **When to apply**:
455
- - Hash dictionaries / content fingerprints (e.g., `.template-hashes.json`)
456
- - Cache files where stale entries are recomputable from authoritative source
457
- - Any cross-OS persisted file where format change correlates with cross-platform fixes
458
-
459
- **When NOT to apply** — if losing the data hurts the user (task state, drafts, settings the user typed). Use real migration there. Sentinel + discard is only safe when data is recomputable.
460
-
461
- **Reference**: `packages/cli/src/utils/template-hash.ts` v2 envelope.
462
-
463
- ---
464
-
465
- ## JSON/External Data Defensive Checks
466
-
467
- When parsing JSON or external data, TypeScript types are **compile-time only**. Runtime data may not match.
468
-
469
- **Rule**: Always add defensive checks for required fields before using them.
470
-
471
- ```typescript
472
- // BAD - Trusts TypeScript type definition
473
- interface MigrationItem {
474
- from: string; // TypeScript says required
475
- to?: string;
476
- }
477
-
478
- function process(item: MigrationItem) {
479
- const path = item.from; // Runtime: could be undefined!
480
- }
481
-
482
- // GOOD - Defensive check before use
483
- function process(item: MigrationItem) {
484
- if (!item.from) return; // Skip invalid data
485
- const path = item.from; // Now guaranteed
486
- }
487
- ```
488
-
489
- **When to apply**:
490
- - Parsing JSON files (manifests, configs)
491
- - API responses
492
- - User input
493
- - Any data from external sources
494
-
495
- **Pattern**: Check existence → then use
496
-
497
- ```typescript
498
- // Filter pattern - skip invalid items
499
- const validItems = items.filter(item => item.from && item.to);
500
-
501
- // Early return pattern - bail on invalid
502
- if (!data.requiredField) {
503
- console.warn("Missing required field");
504
- return defaultValue;
505
- }
506
- ```
507
-
508
- ---
509
-
510
- ## Common Mistakes
511
-
512
- ### 1. "It works on my Mac"
513
-
514
- ```python
515
- # Developer's Mac
516
- subprocess.run(["./script.py"]) # Works!
517
-
518
- # User's Windows
519
- subprocess.run(["./script.py"]) # FileNotFoundError
520
- ```
521
-
522
- ### 2. "The shebang should handle it"
523
-
524
- ```python
525
- #!/usr/bin/env python3
526
- # This line is IGNORED on Windows
527
- ```
528
-
529
- ### 3. "I updated the template"
530
-
531
- ```
532
- src/templates/script.py ← Updated
533
- .trellis/scripts/script.py ← Forgot to sync!
534
- ```
535
-
536
- ### 4. "Python 3 is always python3"
537
-
538
- ```bash
539
- # Developer's Mac/Linux
540
- python3 script.py # Works!
541
-
542
- # User's Windows (Python from python.org)
543
- python3 script.py # 'python3' is not recognized
544
- python script.py # Works!
545
-
546
- # Trellis docs/config should say the rule, not guess one alias everywhere
547
- {{PYTHON_CMD}} script.py
548
- ```
549
-
550
- ### 5. "UTF-8 is the default everywhere"
551
-
552
- ```python
553
- # Developer's Mac (UTF-8 default)
554
- subprocess.run(cmd, capture_output=True, text=True) # Works!
555
-
556
- # User's Windows (GBK/CP1252 default)
557
- subprocess.run(cmd, capture_output=True, text=True) # Garbled Chinese/Unicode
558
- ```
559
-
560
- > **Note**: stdout encoding is also affected. See `backend/script-conventions.md` for the fix.
561
-
562
- ---
563
-
564
- ## Recovery: When You Find a Platform Bug
565
-
566
- 1. **Fix the immediate issue**
567
- 2. **Search for similar patterns** (grep the codebase)
568
- 3. **Update this guide** with the new pattern
569
- 4. **Add to pre-commit checklist** if recurring
570
-
571
- ---
572
-
573
- **Core Principle**: If it's not explicit, it's an assumption. And assumptions break.
574
-
575
- ---
576
-
577
- ## Release Checklist: Versioned Files
578
-
579
- When releasing a new version, ensure **all versioned files** are created/updated:
580
-
581
- - [ ] `src/migrations/manifests/{version}.json` - Migration manifest exists
582
- - [ ] Manifest has correct version, description, changelog
583
- - [ ] `pnpm build` copies manifests to `dist/`
584
- - [ ] Test upgrade path from older versions (not just adjacent)
585
-
586
- **Why this matters**: Missing manifests cause "path undefined" errors when users upgrade from older versions.
587
-
588
- ```bash
589
- # Verify all expected manifests exist
590
- ls src/migrations/manifests/
591
-
592
- # Test upgrade path
593
- node -e "
594
- const { getMigrationsForVersion } = require('./dist/migrations/index.js');
595
- console.log('From 0.2.12:', getMigrationsForVersion('0.2.12', 'CURRENT').length);
596
- "
597
- ```
598
-
599
- ## Release Checklist: Bundled Assets
600
-
601
- When release notes or docs claim an asset is bundled, installed automatically, or
602
- included with Trellis, verify the whole distribution path:
603
-
604
- - [ ] Source file exists in the branch being tagged, not only in another branch,
605
- docs submodule, or marketplace tree.
606
- - [ ] `pnpm build` copies the asset into `dist/templates/**`.
607
- - [ ] `npm pack --dry-run --json` includes the expected `dist/**` path.
608
- - [ ] The built binary installs the asset in a fresh temp repository.
609
- - [ ] `.trellis/.template-hashes.json` tracks the generated asset path.
610
- - [ ] `trellis update --dry-run` reports `Already up to date!` in that temp
611
- repository.
612
-
613
- **Why this matters**: docs/changelog text can move independently from the code
614
- branch that owns distributable templates. A feature can be documented as bundled
615
- while the published npm tarball still lacks the files.
616
-
617
- ```bash
618
- pnpm --filter @blxzer/cursor-trellis build
619
-
620
- cd packages/cli
621
- npm pack --dry-run --json | grep 'dist/templates/common/bundled-skills/<skill>/SKILL.md'
622
- cd ../..
623
-
624
- tmpdir=$(mktemp -d /tmp/trellis-built-bin-smoke-XXXXXX)
625
- printf '{"name":"trellis-smoke","version":"0.0.0"}\n' > "$tmpdir/package.json"
626
- git -C "$tmpdir" init -q
627
- (
628
- cd "$tmpdir"
629
- node /path/to/Trellis/packages/cli/bin/trellis.js init -u smoke --yes --cursor
630
- test -f .cursor/skills/<skill>/SKILL.md
631
- grep -q '<skill>' .trellis/.template-hashes.json
632
- node /path/to/Trellis/packages/cli/bin/trellis.js update --dry-run
633
- )
634
- ```
1
+ # Cross-Platform Thinking Guide (OS / path)
2
+
3
+ > **Purpose**: Catch OS and path assumptions before they become bugs.
4
+ >
5
+ > **Note (cursor-only fork):** This guide covers **operating-system and filesystem** portability (Windows vs macOS/Linux). AI platform adapter sync is Cursor-only; see `cross-layer-thinking-guide.md` for template consistency.
6
+
7
+ ---
8
+
9
+ ## Why This Matters
10
+
11
+ **Most cross-platform bugs come from implicit assumptions**:
12
+
13
+ - Assumed shebang works → breaks on Windows
14
+ - Assumed `/` path separator → breaks on Windows
15
+ - Assumed `\n` line endings → inconsistent behavior
16
+ - Assumed command availability → `grep` vs `findstr`
17
+
18
+ ---
19
+
20
+ ## Platform Differences Checklist
21
+
22
+ ### 1. Script Execution
23
+
24
+ | Assumption | macOS/Linux | Windows |
25
+ |------------|-------------|---------|
26
+ | Shebang (`#!/usr/bin/env python3`) | ✅ Works | ❌ Ignored |
27
+ | Direct execution (`./script.py`) | ✅ Works | ❌ Fails |
28
+ | `python3` command | ✅ Always available | ⚠️ May need `python` |
29
+ | `python` command | ⚠️ May be Python 2 | ✅ Usually Python 3 |
30
+
31
+ **Rule 1**: For user-facing docs, help text, and error messages, either:
32
+
33
+ - state the platform rule explicitly (`python` on Windows, `python3` elsewhere), or
34
+ - render the command through the same platform-aware helper / placeholder the code uses.
35
+
36
+ ```python
37
+ # BAD - Assumes shebang works
38
+ print("Usage: ./script.py <args>")
39
+ print("Run: script.py <args>")
40
+
41
+ # GOOD - Platform-aware wording
42
+ print("Usage: python on Windows, python3 elsewhere")
43
+ print("Run: {{PYTHON_CMD}} ./.trellis/scripts/task.py <args>")
44
+ ```
45
+
46
+ **Rule 2**: When generating config files at init time, use placeholder + platform detection:
47
+
48
+ ```typescript
49
+ // In template file (settings.json):
50
+ { "command": "{{PYTHON_CMD}} .cursor/hooks/script.py" }
51
+
52
+ // In configurator:
53
+ function getPythonCommand(): string {
54
+ return process.platform === "win32" ? "python" : "python3";
55
+ }
56
+
57
+ function replacePlaceholders(content: string): string {
58
+ return content.replace(/\{\{PYTHON_CMD\}\}/g, getPythonCommand());
59
+ }
60
+ ```
61
+
62
+ **Rule 3**: When calling Python at runtime from JavaScript, detect platform dynamically:
63
+
64
+ ```javascript
65
+ import { platform } from "os"
66
+
67
+ const PYTHON_CMD = platform() === "win32" ? "python" : "python3"
68
+ execSync(`${PYTHON_CMD} "${scriptPath}"`, { ... })
69
+ ```
70
+
71
+ **Rule 4**: If you need to verify Python is actually installed (not just choose
72
+ the command), probe the same platform-selected alias you will later render or
73
+ execute:
74
+
75
+ ```typescript
76
+ function getPythonCommand(platform = process.platform): string {
77
+ return platform === "win32" ? "python" : "python3";
78
+ }
79
+
80
+ function warnIfPythonTooOld(): void {
81
+ const cmd = getPythonCommand();
82
+ try {
83
+ execSync(`${cmd} --version`, { stdio: "pipe" });
84
+ } catch {
85
+ // Missing Python is a separate error path; don't silently swap aliases.
86
+ }
87
+ }
88
+ ```
89
+
90
+ **Rule 5**: Don't assume the Python version the AI CLI uses matches your shell's `python3`. The user's terminal may resolve `python3` → homebrew 3.11, but Cursor spawns hook subprocesses with a minimal PATH that resolves `python3` → `/usr/bin/python3` → macOS system 3.9. Distributed templates must either target the lowest plausible version or use `from __future__ import annotations` for PEP 604 syntax. See `cli/backend/script-conventions.md` → **CRITICAL: PEP 604 Annotations Require `from __future__ import annotations`** for the hard rule and audit check.
91
+
92
+ **Rule 6**: When calling Python from Python, use `sys.executable`:
93
+
94
+ ```python
95
+ import sys
96
+ import subprocess
97
+
98
+ # BAD - Hardcoded command
99
+ subprocess.run(["python3", "other_script.py"])
100
+
101
+ # GOOD - Use current interpreter
102
+ subprocess.run([sys.executable, "other_script.py"])
103
+ ```
104
+
105
+ ### 2. Path Handling
106
+
107
+ | Assumption | macOS/Linux | Windows |
108
+ |------------|-------------|---------|
109
+ | `/` separator | ✅ Works | ⚠️ Sometimes works |
110
+ | `\` separator | ❌ Escape char | ✅ Native |
111
+ | `pathlib.Path` | ✅ Works | ✅ Works |
112
+
113
+ **Rule (Python)**: Use `pathlib.Path` for all path operations.
114
+
115
+ ```python
116
+ # BAD - String concatenation
117
+ path = base + "/" + filename
118
+
119
+ # GOOD - pathlib
120
+ from pathlib import Path
121
+ path = Path(base) / filename
122
+ ```
123
+
124
+ #### Logical key vs filesystem path (TypeScript)
125
+
126
+ A path string has two distinct roles. **Treat them differently.**
127
+
128
+ | Role | OS-native (`\` on Windows) | Always POSIX (`/`) |
129
+ |------|---------------------------|--------------------|
130
+ | `fs.readFileSync(p)` / `path.join(cwd, x)` for fs call | ✅ Required | ❌ May fail on Windows |
131
+ | `Map<relPath, content>` key, JSON field, hash dictionary key, anything persisted across OS | ❌ Cross-OS mismatch | ✅ Required |
132
+
133
+ **Rule**: Anywhere a path string crosses OS or persists (Map keys consumed by another OS, JSON fields, hash dictionary keys), normalize to POSIX. Anywhere it goes straight to `fs.*`, leave OS-native.
134
+
135
+ **Single source of truth**: `packages/cli/src/utils/posix.ts` exports `toPosix(p)`. Don't sprinkle `replaceAll('\\', '/')` at every `path.join` site — apply `toPosix` **once at the boundary**: collector exit (Map key entering hash dictionary) or write-time (`saveHashes` before `JSON.stringify`).
136
+
137
+ ```typescript
138
+ // BAD - logical key carries OS-native separator
139
+ function collectTemplates(): Map<string, string> {
140
+ const files = new Map<string, string>();
141
+ for (const entry of walk(dir)) {
142
+ files.set(path.join(".cursor", entry), readFile(entry)); // \ on Windows
143
+ }
144
+ return files;
145
+ }
146
+
147
+ // GOOD - normalize at the boundary
148
+ import { toPosix } from "../utils/posix.js";
149
+
150
+ function collectTemplates(): Map<string, string> {
151
+ const files = new Map<string, string>();
152
+ for (const entry of walk(dir)) {
153
+ files.set(toPosix(path.join(".cursor", entry)), readFile(entry));
154
+ }
155
+ return files;
156
+ }
157
+
158
+ // ALSO ACCEPTABLE - write-side defense (for storage helpers like saveHashes)
159
+ function saveHashes(cwd: string, hashes: Record<string, string>): void {
160
+ const normalized = Object.fromEntries(
161
+ Object.entries(hashes).map(([k, v]) => [toPosix(k), v])
162
+ );
163
+ fs.writeFileSync(getHashesPath(cwd), JSON.stringify(normalized, null, 2));
164
+ }
165
+ ```
166
+
167
+ **Common offender**: `path.relative(cwd, fullPath)` produces `\` on Windows. If you then use that string as a hash dictionary lookup key (`hashes[relPath]`), `toPosix` it first, or the lookup misses on Windows.
168
+
169
+ ### 3. Line Endings
170
+
171
+ | Format | macOS/Linux | Windows | Git |
172
+ |--------|-------------|---------|-----|
173
+ | `\n` (LF) | ✅ Native | ⚠️ Some tools | ✅ Normalized |
174
+ | `\r\n` (CRLF) | ⚠️ Extra char | ✅ Native | Converted |
175
+
176
+ **Rule 1**: Use `.gitattributes` to enforce consistent line endings.
177
+
178
+ ```gitattributes
179
+ * text=auto eol=lf
180
+ *.sh text eol=lf
181
+ *.py text eol=lf
182
+ ```
183
+
184
+ **Rule 2**: When hashing or comparing **content** across platforms, normalize line endings before computing the hash. `.gitattributes` only governs git checkout — files written by users, scripts, or `core.autocrlf=true` may still arrive as CRLF, and `sha256(LF)` ≠ `sha256(CRLF)` for otherwise-identical content.
185
+
186
+ ```typescript
187
+ // BAD - Windows users with autocrlf=true get a different hash
188
+ export function computeHash(content: string): string {
189
+ return createHash("sha256").update(content, "utf-8").digest("hex");
190
+ }
191
+
192
+ // GOOD - normalize before hashing so logical content hashes identically
193
+ export function computeHash(content: string): string {
194
+ const normalized = content.replace(/\r\n/g, "\n");
195
+ return createHash("sha256").update(normalized, "utf-8").digest("hex");
196
+ }
197
+ ```
198
+
199
+ Apply this rule wherever the hash crosses OS boundaries (template hash dictionary, content fingerprints stored in JSON, integrity checks against a remote registry).
200
+
201
+ ### 4. Environment Variables
202
+
203
+ | Variable | macOS/Linux | Windows |
204
+ |----------|-------------|---------|
205
+ | `HOME` | ✅ Set | ❌ Use `USERPROFILE` |
206
+ | `PATH` separator | `:` | `;` |
207
+ | Case sensitivity | ✅ Case-sensitive | ❌ Case-insensitive |
208
+
209
+ **Rule 1**: Use `pathlib.Path.home()` instead of environment variables.
210
+
211
+ ```python
212
+ # BAD
213
+ home = os.environ.get("HOME")
214
+
215
+ # GOOD
216
+ home = Path.home()
217
+ ```
218
+
219
+ **Rule 2**: When injecting environment variables into shell commands, generate
220
+ the prefix for the actual shell that will parse the command. Do not choose
221
+ syntax from OS alone. AI tool "Bash" surfaces on Windows may execute through
222
+ PowerShell, Git Bash, MSYS2, or another POSIX-like shell.
223
+
224
+ ```javascript
225
+ // BAD - breaks when the host shell is PowerShell
226
+ command = `export TRELLIS_CONTEXT_ID=${shellQuote(contextKey)}; ${command}`;
227
+
228
+ // GOOD - shell-dialect-aware command prefix
229
+ const prefix = process.platform === "win32" && !isWindowsPosixShell(process.env)
230
+ ? `$env:TRELLIS_CONTEXT_ID = ${powershellQuote(contextKey)}; `
231
+ : `export TRELLIS_CONTEXT_ID=${shellQuote(contextKey)}; `;
232
+ command = `${prefix}${command}`;
233
+ ```
234
+
235
+ On Windows, treat `MSYSTEM`, `MINGW_PREFIX`, `OSTYPE=msys|mingw|cygwin`,
236
+ `SHELL=...bash`, or a platform-specific Git Bash setting as POSIX-shell
237
+ signals. Keep PowerShell as the Windows default when there is no POSIX-shell
238
+ signal.
239
+
240
+ Also make duplicate-injection detection shell-aware. A guard that only matches
241
+ `export VAR=` will miss PowerShell's `$env:VAR = ...` form and can wrap an
242
+ already-correct command a second time.
243
+
244
+ ### 5. Command Availability
245
+
246
+ | Command | macOS/Linux | Windows |
247
+ |---------|-------------|---------|
248
+ | `grep` | ✅ Built-in | ❌ Not available |
249
+ | `find` | ✅ Built-in | ⚠️ Different syntax |
250
+ | `cat` | ✅ Built-in | ❌ Use `type` |
251
+ | `tail -f` | ✅ Built-in | ❌ Not available |
252
+
253
+ **Rule**: Use Python standard library instead of shell commands when possible.
254
+
255
+ ```python
256
+ # BAD - tail -f is not available on Windows
257
+ subprocess.run(["tail", "-f", log_file])
258
+
259
+ # GOOD - Cross-platform implementation
260
+ def tail_follow(file_path: Path) -> None:
261
+ """Follow a file like 'tail -f', cross-platform compatible."""
262
+ with open(file_path, "r", encoding="utf-8", errors="replace") as f:
263
+ f.seek(0, 2) # Go to end
264
+ while True:
265
+ line = f.readline()
266
+ if line:
267
+ print(line, end="", flush=True)
268
+ else:
269
+ time.sleep(0.1)
270
+ ```
271
+
272
+ ### Optional Advisory Checks in Agent Sandboxes
273
+
274
+ AI CLI subprocesses may run with outbound network disabled even when the user's
275
+ normal terminal has network access. Prefer local CLI probes over optional
276
+ network probes when the local CLI already exposes the needed information.
277
+
278
+ **Rule 1**: Do not let a failed optional advisory check consume a once-per-session
279
+ marker. Write the marker only after the script resolves a usable value and can
280
+ make the intended decision. Otherwise a transient sandbox/network failure hides
281
+ the hint for the rest of the session.
282
+
283
+ **Rule 2**: If a local command can provide the needed value, try it with a short
284
+ timeout and captured output. For example, `cstl --version` already runs the
285
+ CLI's version comparison logic and can support an actionable update prompt
286
+ without duplicating npm registry parsing.
287
+
288
+ **Rule 3**: Keep advisory checks silent on failure. The user-facing context output
289
+ must not fail or become noisy because an advisory check could not complete.
290
+
291
+ ### 6. File Encoding
292
+
293
+ | Default Encoding | macOS/Linux | Windows |
294
+ |------------------|-------------|---------|
295
+ | Terminal | UTF-8 | Often CP1252 or GBK |
296
+ | File I/O | UTF-8 | System locale |
297
+ | Git output | UTF-8 | May vary |
298
+
299
+ **Rule**: Always explicitly specify `encoding="utf-8"` and use `errors="replace"`.
300
+
301
+ > **Checklist**: When writing scripts that print non-ASCII, did you configure stdout encoding?
302
+ > See `backend/script-conventions.md` for the specific pattern.
303
+
304
+ ```python
305
+ # BAD - Relies on system default
306
+ with open(file, "r") as f:
307
+ content = f.read()
308
+
309
+ result = subprocess.run(cmd, capture_output=True, text=True)
310
+
311
+ # GOOD - Explicit encoding with error handling
312
+ with open(file, "r", encoding="utf-8", errors="replace") as f:
313
+ content = f.read()
314
+
315
+ result = subprocess.run(
316
+ cmd,
317
+ capture_output=True,
318
+ text=True,
319
+ encoding="utf-8",
320
+ errors="replace"
321
+ )
322
+ ```
323
+
324
+ **Git commands**: Force UTF-8 output encoding:
325
+
326
+ ```python
327
+ # Force git to output UTF-8
328
+ git_args = ["git", "-c", "i18n.logOutputEncoding=UTF-8"] + args
329
+ result = subprocess.run(
330
+ git_args,
331
+ capture_output=True,
332
+ text=True,
333
+ encoding="utf-8",
334
+ errors="replace"
335
+ )
336
+ ```
337
+
338
+ ---
339
+
340
+ ## Change Propagation Checklist
341
+
342
+ When making platform-related changes, check **all these locations**:
343
+
344
+ ### Commands / Skills Sync
345
+ - [ ] New command/skill added to the Cursor templates (`src/templates/cursor/` and `src/templates/common/`)
346
+ - [ ] Cursor test file updated with new entry in `EXPECTED_COMMAND_NAMES` / `EXPECTED_SKILL_NAMES`
347
+ - [ ] Command format matches Cursor convention (see `platform-integration.md` → Command Format)
348
+ - [ ] If the command exists in the dogfooded copy (`.cursor/`), sync it too
349
+
350
+ ### Documentation & Help Text
351
+ - [ ] Docstrings at top of Python files
352
+ - [ ] `--help` output / argparse descriptions
353
+ - [ ] Usage examples in README
354
+ - [ ] Error messages that suggest commands
355
+ - [ ] Markdown documentation (`.md` files)
356
+
357
+ ### Code Locations
358
+ - [ ] `src/templates/` - Template files for new projects
359
+ - [ ] `.trellis/scripts/` - Project's own scripts (if self-hosting)
360
+ - [ ] `dist/` - Built output (rebuild after changes)
361
+
362
+ ### Search Pattern
363
+ ```bash
364
+ # Find all places that might need updating
365
+ grep -r "python [a-z]" --include="*.py" --include="*.md"
366
+ grep -r "{{PYTHON_CMD}}\\|python3\\|python " --include="*.py" --include="*.md"
367
+ ```
368
+
369
+ ---
370
+
371
+ ## Pre-Commit Checklist
372
+
373
+ Before committing cross-platform code:
374
+
375
+ - [ ] User-facing Python invocations are platform-aware (`python` on Windows, `python3` elsewhere) or use `{{PYTHON_CMD}}`
376
+ - [ ] Python subprocesses from Python use `sys.executable`
377
+ - [ ] All paths use `pathlib.Path`
378
+ - [ ] No hardcoded path separators (`/` or `\`)
379
+ - [ ] Path strings used as logical/persisted keys (Map keys, JSON fields, hash dictionary keys) are normalized via `toPosix()`; `fs.*` calls keep OS-native paths
380
+ - [ ] Content hashes computed across OSes normalize line endings (`\r\n` → `\n`) before hashing
381
+ - [ ] Cross-OS JSON with potential legacy pollution carries a `__version` sentinel and the loader discards unknown/legacy versions
382
+ - [ ] No platform-specific commands without fallbacks (e.g., `tail -f`)
383
+ - [ ] Optional advisory checks do not burn once-per-session markers on failure
384
+ - [ ] All file I/O specifies `encoding="utf-8"` and `errors="replace"`
385
+ - [ ] All subprocess calls specify `encoding="utf-8"` and `errors="replace"`
386
+ - [ ] Git commands use `-c i18n.logOutputEncoding=UTF-8`
387
+ - [ ] External tool API formats verified from documentation
388
+ - [ ] Documentation matches code behavior
389
+ - [ ] Ran search to find all affected locations
390
+
391
+ ### 7. External Tool API Contracts
392
+
393
+ When integrating with external tools (Cursor, etc.), their API contracts are **implicit assumptions**.
394
+
395
+ **Rule**: Verify API formats from official documentation, don't guess.
396
+
397
+ ```python
398
+ # BAD - Guessed format
399
+ output = {"continue": True, "message": "..."}
400
+
401
+ # GOOD - Verified format from documentation
402
+ output = {
403
+ "hookSpecificOutput": {
404
+ "hookEventName": "SessionStart",
405
+ "additionalContext": "..."
406
+ }
407
+ }
408
+ ```
409
+
410
+ > **Warning**: Different hook types may have different output formats.
411
+ > Always check the specific documentation for each hook event.
412
+
413
+ ---
414
+
415
+ ## Cross-Platform Persisted JSON: Schema Migration Sentinel
416
+
417
+ When a JSON file may be read/written across OSes (committed to git, synced via cloud, copied between machines) **and an older format may already exist on user disks with cross-platform pollution** (Windows-style keys, CRLF-derived hashes, locale-encoded strings), add a `__version` sentinel and let the loader discard old formats so the writer regenerates clean data.
418
+
419
+ **Why not migrate-in-place?** Path-key migration (`\\` → `/`) plus hash-input migration (CRLF → LF re-hash) plus encoding fixes are correlated — trying to translate the old payload risks producing wrong values. Discarding and regenerating is **safe**: the data is recomputable from disk, and `loadX` returning `{}` triggers the existing init/update path to rebuild canonical entries.
420
+
421
+ ```typescript
422
+ const SCHEMA_VERSION = 2;
423
+ type StoredV2 = { __version: number; hashes: Record<string, string> };
424
+
425
+ export function loadHashes(cwd: string): Record<string, string> {
426
+ const file = getHashesPath(cwd);
427
+ if (!fs.existsSync(file)) return {};
428
+
429
+ try {
430
+ const parsed = JSON.parse(fs.readFileSync(file, "utf-8")) as unknown;
431
+
432
+ // Reject legacy flat format (no __version) and unknown versions.
433
+ // The next saveHashes / initializeHashes will write a fresh v2 file.
434
+ if (
435
+ !parsed ||
436
+ typeof parsed !== "object" ||
437
+ (parsed as StoredV2).__version !== SCHEMA_VERSION ||
438
+ typeof (parsed as StoredV2).hashes !== "object"
439
+ ) {
440
+ return {};
441
+ }
442
+ return (parsed as StoredV2).hashes;
443
+ } catch {
444
+ return {};
445
+ }
446
+ }
447
+
448
+ export function saveHashes(cwd: string, hashes: Record<string, string>): void {
449
+ const payload: StoredV2 = { __version: SCHEMA_VERSION, hashes };
450
+ fs.writeFileSync(getHashesPath(cwd), JSON.stringify(payload, null, 2));
451
+ }
452
+ ```
453
+
454
+ **When to apply**:
455
+ - Hash dictionaries / content fingerprints (e.g., `.template-hashes.json`)
456
+ - Cache files where stale entries are recomputable from authoritative source
457
+ - Any cross-OS persisted file where format change correlates with cross-platform fixes
458
+
459
+ **When NOT to apply** — if losing the data hurts the user (task state, drafts, settings the user typed). Use real migration there. Sentinel + discard is only safe when data is recomputable.
460
+
461
+ **Reference**: `packages/cli/src/utils/template-hash.ts` v2 envelope.
462
+
463
+ ---
464
+
465
+ ## JSON/External Data Defensive Checks
466
+
467
+ When parsing JSON or external data, TypeScript types are **compile-time only**. Runtime data may not match.
468
+
469
+ **Rule**: Always add defensive checks for required fields before using them.
470
+
471
+ ```typescript
472
+ // BAD - Trusts TypeScript type definition
473
+ interface MigrationItem {
474
+ from: string; // TypeScript says required
475
+ to?: string;
476
+ }
477
+
478
+ function process(item: MigrationItem) {
479
+ const path = item.from; // Runtime: could be undefined!
480
+ }
481
+
482
+ // GOOD - Defensive check before use
483
+ function process(item: MigrationItem) {
484
+ if (!item.from) return; // Skip invalid data
485
+ const path = item.from; // Now guaranteed
486
+ }
487
+ ```
488
+
489
+ **When to apply**:
490
+ - Parsing JSON files (manifests, configs)
491
+ - API responses
492
+ - User input
493
+ - Any data from external sources
494
+
495
+ **Pattern**: Check existence → then use
496
+
497
+ ```typescript
498
+ // Filter pattern - skip invalid items
499
+ const validItems = items.filter(item => item.from && item.to);
500
+
501
+ // Early return pattern - bail on invalid
502
+ if (!data.requiredField) {
503
+ console.warn("Missing required field");
504
+ return defaultValue;
505
+ }
506
+ ```
507
+
508
+ ---
509
+
510
+ ## Common Mistakes
511
+
512
+ ### 1. "It works on my Mac"
513
+
514
+ ```python
515
+ # Developer's Mac
516
+ subprocess.run(["./script.py"]) # Works!
517
+
518
+ # User's Windows
519
+ subprocess.run(["./script.py"]) # FileNotFoundError
520
+ ```
521
+
522
+ ### 2. "The shebang should handle it"
523
+
524
+ ```python
525
+ #!/usr/bin/env python3
526
+ # This line is IGNORED on Windows
527
+ ```
528
+
529
+ ### 3. "I updated the template"
530
+
531
+ ```
532
+ src/templates/script.py ← Updated
533
+ .trellis/scripts/script.py ← Forgot to sync!
534
+ ```
535
+
536
+ ### 4. "Python 3 is always python3"
537
+
538
+ ```bash
539
+ # Developer's Mac/Linux
540
+ python3 script.py # Works!
541
+
542
+ # User's Windows (Python from python.org)
543
+ python3 script.py # 'python3' is not recognized
544
+ python script.py # Works!
545
+
546
+ # Trellis docs/config should say the rule, not guess one alias everywhere
547
+ {{PYTHON_CMD}} script.py
548
+ ```
549
+
550
+ ### 5. "UTF-8 is the default everywhere"
551
+
552
+ ```python
553
+ # Developer's Mac (UTF-8 default)
554
+ subprocess.run(cmd, capture_output=True, text=True) # Works!
555
+
556
+ # User's Windows (GBK/CP1252 default)
557
+ subprocess.run(cmd, capture_output=True, text=True) # Garbled Chinese/Unicode
558
+ ```
559
+
560
+ > **Note**: stdout encoding is also affected. See `backend/script-conventions.md` for the fix.
561
+
562
+ ---
563
+
564
+ ## Recovery: When You Find a Platform Bug
565
+
566
+ 1. **Fix the immediate issue**
567
+ 2. **Search for similar patterns** (grep the codebase)
568
+ 3. **Update this guide** with the new pattern
569
+ 4. **Add to pre-commit checklist** if recurring
570
+
571
+ ---
572
+
573
+ **Core Principle**: If it's not explicit, it's an assumption. And assumptions break.
574
+
575
+ ---
576
+
577
+ ## Release Checklist: Versioned Files
578
+
579
+ When releasing a new version, ensure **all versioned files** are created/updated:
580
+
581
+ - [ ] `src/migrations/manifests/{version}.json` - Migration manifest exists
582
+ - [ ] Manifest has correct version, description, changelog
583
+ - [ ] `pnpm build` copies manifests to `dist/`
584
+ - [ ] Test upgrade path from older versions (not just adjacent)
585
+
586
+ **Why this matters**: Missing manifests cause "path undefined" errors when users upgrade from older versions.
587
+
588
+ ```bash
589
+ # Verify all expected manifests exist
590
+ ls src/migrations/manifests/
591
+
592
+ # Test upgrade path
593
+ node -e "
594
+ const { getMigrationsForVersion } = require('./dist/migrations/index.js');
595
+ console.log('From 0.2.12:', getMigrationsForVersion('0.2.12', 'CURRENT').length);
596
+ "
597
+ ```
598
+
599
+ ## Release Checklist: Bundled Assets
600
+
601
+ When release notes or docs claim an asset is bundled, installed automatically, or
602
+ included with Trellis, verify the whole distribution path:
603
+
604
+ - [ ] Source file exists in the branch being tagged, not only in another branch,
605
+ docs submodule, or marketplace tree.
606
+ - [ ] `pnpm build` copies the asset into `dist/templates/**`.
607
+ - [ ] `npm pack --dry-run --json` includes the expected `dist/**` path.
608
+ - [ ] The built binary installs the asset in a fresh temp repository.
609
+ - [ ] `.trellis/.template-hashes.json` tracks the generated asset path.
610
+ - [ ] `cstl update --dry-run` reports `Already up to date!` in that temp
611
+ repository.
612
+
613
+ **Why this matters**: docs/changelog text can move independently from the code
614
+ branch that owns distributable templates. A feature can be documented as bundled
615
+ while the published npm tarball still lacks the files.
616
+
617
+ ```bash
618
+ pnpm --filter @blxzer/cursor-trellis build
619
+
620
+ cd packages/cli
621
+ npm pack --dry-run --json | grep 'dist/templates/common/bundled-skills/<skill>/SKILL.md'
622
+ cd ../..
623
+
624
+ tmpdir=$(mktemp -d /tmp/trellis-built-bin-smoke-XXXXXX)
625
+ printf '{"name":"trellis-smoke","version":"0.0.0"}\n' > "$tmpdir/package.json"
626
+ git -C "$tmpdir" init -q
627
+ (
628
+ cd "$tmpdir"
629
+ node /path/to/Trellis/packages/cli/bin/trellis.js init -u smoke --yes --cursor
630
+ test -f .cursor/skills/<skill>/SKILL.md
631
+ grep -q '<skill>' .trellis/.template-hashes.json
632
+ node /path/to/Trellis/packages/cli/bin/trellis.js update --dry-run
633
+ )
634
+ ```