| 1 | --- |
| 2 | name: "windows-compatibility" |
| 3 | description: "Cross-platform path handling and command patterns" |
| 4 | domain: "platform" |
| 5 | confidence: "high" |
| 6 | source: "earned (multiple Windows-specific bugs: colons in filenames, git -C failures, path separators)" |
| 7 | --- |
| 8 | |
| 9 | ## Context |
| 10 | |
| 11 | Squad runs on Windows, macOS, and Linux. Several bugs have been traced to platform-specific assumptions: ISO timestamps with colons (illegal on Windows), `git -C` with Windows paths (unreliable), forward-slash paths in Node.js on Windows. |
| 12 | |
| 13 | ## Patterns |
| 14 | |
| 15 | ### Filenames & Timestamps |
| 16 | - **Never use colons in filenames:** ISO 8601 format `2026-03-15T05:30:00Z` is illegal on Windows |
| 17 | - **Use `safeTimestamp()` utility:** Replaces colons with hyphens → `2026-03-15T05-30-00Z` |
| 18 | - **Centralize formatting:** Don't inline `.toISOString().replace(/:/g, '-')` — use the utility |
| 19 | |
| 20 | ### Git Commands |
| 21 | - **Never use `git -C {path}`:** Unreliable with Windows paths (backslashes, spaces, drive letters) |
| 22 | - **Always `cd` first:** Change directory, then run git commands |
| 23 | - **Check for changes before commit:** `git diff --cached --quiet` (exit 0 = no changes) |
| 24 | |
| 25 | ### Commit Messages |
| 26 | - **Never embed newlines in `-m` flag:** Backtick-n (`\n`) fails silently in PowerShell |
| 27 | - **Use temp file + `-F` flag:** Write message to file, commit with `git commit -F $msgFile` |
| 28 | |
| 29 | ### Paths |
| 30 | - **Never assume CWD is repo root:** Always use `TEAM ROOT` from spawn prompt or run `git rev-parse --show-toplevel` |
| 31 | - **Use path.join() or path.resolve():** Don't manually concatenate with `/` or `\` |
| 32 | |
| 33 | ### Path Comparison (Case Sensitivity) |
| 34 | - **Never use case-sensitive `startsWith` or `===` for path comparison on Windows or macOS:** These filesystems are case-insensitive — `C:\Users\` and `c:\users\` refer to the same location |
| 35 | - **Use platform-aware comparison:** Check `process.platform === 'win32' || process.platform === 'darwin'` and lowercase both sides before comparing |
| 36 | - **Pattern:** |
| 37 | ```typescript |
| 38 | const CASE_INSENSITIVE = process.platform === 'win32' || process.platform === 'darwin'; |
| 39 | |
| 40 | function pathStartsWith(fullPath: string, prefix: string): boolean { |
| 41 | if (CASE_INSENSITIVE) { |
| 42 | return fullPath.toLowerCase().startsWith(prefix.toLowerCase()); |
| 43 | } |
| 44 | return fullPath.startsWith(prefix); |
| 45 | } |
| 46 | ``` |
| 47 | - **Where it matters:** Security checks (path traversal prevention), rootDir confinement, any path-contains-path validation |
| 48 | - **Linux is case-sensitive:** Do NOT lowercase on Linux — `/Home/` and `/home/` are different directories |
| 49 | |
| 50 | ## Examples |
| 51 | |
| 52 | ✓ **Correct:** |
| 53 | ```javascript |
| 54 | // Timestamp utility |
| 55 | const safeTimestamp = () => new Date().toISOString().replace(/:/g, '-').split('.')[0] + 'Z'; |
| 56 | |
| 57 | // Git workflow (PowerShell) |
| 58 | cd $teamRoot |
| 59 | git add .squad/ |
| 60 | if ($LASTEXITCODE -eq 0) { |
| 61 | $msg = @" |
| 62 | docs(ai-team): session log |
| 63 | |
| 64 | Changes: |
| 65 | - Added decisions |
| 66 | "@ |
| 67 | $msgFile = [System.IO.Path]::GetTempFileName() |
| 68 | Set-Content -Path $msgFile -Value $msg -Encoding utf8 |
| 69 | git commit -F $msgFile |
| 70 | Remove-Item $msgFile |
| 71 | } |
| 72 | ``` |
| 73 | |
| 74 | ✗ **Incorrect:** |
| 75 | ```javascript |
| 76 | // Colon in filename |
| 77 | const logPath = `.squad/log/${new Date().toISOString()}.md`; // ILLEGAL on Windows |
| 78 | |
| 79 | // git -C with Windows path |
| 80 | exec('git -C C:\\src\\squad add .squad/'); // UNRELIABLE |
| 81 | |
| 82 | // Inline newlines in commit message |
| 83 | exec('git commit -m "First line\nSecond line"'); // FAILS silently in PowerShell |
| 84 | ``` |
| 85 | |
| 86 | ## Anti-Patterns |
| 87 | |
| 88 | - Testing only on one platform (bugs ship to other platforms) |
| 89 | - Assuming Unix-style paths work everywhere |
| 90 | - Using `git -C` because it "looks cleaner" (it doesn't work) |
| 91 | - Skipping `git diff --cached --quiet` check (creates empty commits) |
| 92 | - **Wrong — case-sensitive path check on Windows and macOS:** |
| 93 | ```typescript |
| 94 | if (!resolved.startsWith(rootDir + path.sep)) { |
| 95 | throw new Error('Path traversal blocked'); |
| 96 | } |
| 97 | // Fails: 'c:\\Users\\temp\\file'.startsWith('C:\\Users\\temp\\') → false |
| 98 | ``` |