@every-env/compound-plugin 0.8.0 → 0.12.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 (93) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/AGENTS.md +5 -1
  3. package/CHANGELOG.md +50 -0
  4. package/CLAUDE.md +3 -3
  5. package/README.md +52 -14
  6. package/docs/plans/2026-02-14-feat-auto-detect-install-and-gemini-sync-plan.md +360 -0
  7. package/docs/plans/2026-02-25-feat-windsurf-global-scope-support-plan.md +627 -0
  8. package/docs/plans/2026-03-01-feat-ce-command-aliases-backwards-compatible-deprecation-plan.md +261 -0
  9. package/docs/plans/feature_opencode-commands-as-md-and-config-merge.md +574 -0
  10. package/docs/solutions/adding-converter-target-providers.md +692 -0
  11. package/docs/solutions/plugin-versioning-requirements.md +3 -3
  12. package/docs/specs/kiro.md +171 -0
  13. package/docs/specs/windsurf.md +477 -0
  14. package/package.json +1 -1
  15. package/plans/landing-page-launchkit-refresh.md +2 -2
  16. package/plugins/compound-engineering/.claude-plugin/plugin.json +2 -2
  17. package/plugins/compound-engineering/CHANGELOG.md +72 -1
  18. package/plugins/compound-engineering/CLAUDE.md +9 -7
  19. package/plugins/compound-engineering/README.md +10 -7
  20. package/plugins/compound-engineering/agents/research/git-history-analyzer.md +1 -1
  21. package/plugins/compound-engineering/agents/research/learnings-researcher.md +1 -1
  22. package/plugins/compound-engineering/agents/review/code-simplicity-reviewer.md +1 -1
  23. package/plugins/compound-engineering/commands/ce/brainstorm.md +145 -0
  24. package/plugins/compound-engineering/commands/ce/compound.md +240 -0
  25. package/plugins/compound-engineering/commands/ce/plan.md +636 -0
  26. package/plugins/compound-engineering/commands/ce/review.md +525 -0
  27. package/plugins/compound-engineering/commands/ce/work.md +470 -0
  28. package/plugins/compound-engineering/commands/create-agent-skill.md +1 -1
  29. package/plugins/compound-engineering/commands/deepen-plan.md +6 -6
  30. package/plugins/compound-engineering/commands/deploy-docs.md +1 -1
  31. package/plugins/compound-engineering/commands/feature-video.md +15 -6
  32. package/plugins/compound-engineering/commands/heal-skill.md +1 -1
  33. package/plugins/compound-engineering/commands/lfg.md +3 -3
  34. package/plugins/compound-engineering/commands/slfg.md +3 -3
  35. package/plugins/compound-engineering/commands/test-xcode.md +2 -2
  36. package/plugins/compound-engineering/commands/workflows/brainstorm.md +4 -123
  37. package/plugins/compound-engineering/commands/workflows/compound.md +4 -234
  38. package/plugins/compound-engineering/commands/workflows/plan.md +4 -562
  39. package/plugins/compound-engineering/commands/workflows/review.md +4 -522
  40. package/plugins/compound-engineering/commands/workflows/work.md +4 -448
  41. package/plugins/compound-engineering/skills/brainstorming/SKILL.md +3 -3
  42. package/plugins/compound-engineering/skills/document-review/SKILL.md +1 -1
  43. package/plugins/compound-engineering/skills/file-todos/SKILL.md +1 -1
  44. package/plugins/compound-engineering/skills/git-worktree/SKILL.md +5 -5
  45. package/plugins/compound-engineering/skills/proof/SKILL.md +185 -0
  46. package/plugins/compound-engineering/skills/resolve-pr-parallel/SKILL.md +1 -1
  47. package/plugins/compound-engineering/skills/setup/SKILL.md +2 -2
  48. package/src/commands/convert.ts +101 -23
  49. package/src/commands/install.ts +102 -41
  50. package/src/commands/sync.ts +58 -38
  51. package/src/converters/claude-to-kiro.ts +262 -0
  52. package/src/converters/claude-to-openclaw.ts +240 -0
  53. package/src/converters/claude-to-opencode.ts +12 -10
  54. package/src/converters/claude-to-qwen.ts +238 -0
  55. package/src/converters/claude-to-windsurf.ts +205 -0
  56. package/src/sync/gemini.ts +76 -0
  57. package/src/targets/index.ts +69 -1
  58. package/src/targets/kiro.ts +122 -0
  59. package/src/targets/openclaw.ts +96 -0
  60. package/src/targets/opencode.ts +76 -10
  61. package/src/targets/qwen.ts +64 -0
  62. package/src/targets/windsurf.ts +104 -0
  63. package/src/types/kiro.ts +44 -0
  64. package/src/types/openclaw.ts +52 -0
  65. package/src/types/opencode.ts +7 -8
  66. package/src/types/qwen.ts +48 -0
  67. package/src/types/windsurf.ts +34 -0
  68. package/src/utils/detect-tools.ts +46 -0
  69. package/src/utils/files.ts +7 -0
  70. package/src/utils/resolve-output.ts +50 -0
  71. package/src/utils/secrets.ts +24 -0
  72. package/tests/cli.test.ts +78 -0
  73. package/tests/converter.test.ts +43 -10
  74. package/tests/detect-tools.test.ts +96 -0
  75. package/tests/kiro-converter.test.ts +381 -0
  76. package/tests/kiro-writer.test.ts +273 -0
  77. package/tests/openclaw-converter.test.ts +200 -0
  78. package/tests/opencode-writer.test.ts +142 -5
  79. package/tests/qwen-converter.test.ts +238 -0
  80. package/tests/resolve-output.test.ts +131 -0
  81. package/tests/sync-gemini.test.ts +106 -0
  82. package/tests/windsurf-converter.test.ts +573 -0
  83. package/tests/windsurf-writer.test.ts +359 -0
  84. package/docs/css/docs.css +0 -675
  85. package/docs/css/style.css +0 -2886
  86. package/docs/index.html +0 -1046
  87. package/docs/js/main.js +0 -225
  88. package/docs/pages/agents.html +0 -649
  89. package/docs/pages/changelog.html +0 -534
  90. package/docs/pages/commands.html +0 -523
  91. package/docs/pages/getting-started.html +0 -582
  92. package/docs/pages/mcp-servers.html +0 -409
  93. package/docs/pages/skills.html +0 -611
@@ -0,0 +1,627 @@
1
+ ---
2
+ title: Windsurf Global Scope Support
3
+ type: feat
4
+ status: completed
5
+ date: 2026-02-25
6
+ deepened: 2026-02-25
7
+ prior: docs/plans/2026-02-23-feat-add-windsurf-target-provider-plan.md (removed — superseded)
8
+ ---
9
+
10
+ # Windsurf Global Scope Support
11
+
12
+ ## Post-Implementation Revisions (2026-02-26)
13
+
14
+ After auditing the implementation against `docs/specs/windsurf.md`, two significant changes were made:
15
+
16
+ 1. **Agents → Skills (not Workflows)**: Claude agents map to Windsurf Skills (`skills/{name}/SKILL.md`), not Workflows. Skills are "complex multi-step tasks with supporting resources" — a better conceptual match for specialized expertise/personas. Workflows are "reusable step-by-step procedures" — a better match for Claude Commands (slash commands).
17
+
18
+ 2. **Workflows are flat files**: Command workflows are written to `global_workflows/{name}.md` (global scope) or `workflows/{name}.md` (workspace scope). No subdirectories — the spec requires flat files.
19
+
20
+ 3. **Content transforms updated**: `@agent-name` references are kept as-is (Windsurf skill invocation syntax). `/command` references produce `/{name}` (not `/commands/{name}`). `Task agent(args)` produces `Use the @agent-name skill: args`.
21
+
22
+ ### Final Component Mapping (per spec)
23
+
24
+ | Claude Code | Windsurf | Output Path | Invocation |
25
+ |---|---|---|---|
26
+ | Agents (`.md`) | Skills | `skills/{name}/SKILL.md` | `@skill-name` or automatic |
27
+ | Commands (`.md`) | Workflows (flat) | `global_workflows/{name}.md` (global) / `workflows/{name}.md` (workspace) | `/{workflow-name}` |
28
+ | Skills (`SKILL.md`) | Skills (pass-through) | `skills/{name}/SKILL.md` | `@skill-name` |
29
+ | MCP servers | `mcp_config.json` | `mcp_config.json` | N/A |
30
+ | Hooks | Skipped with warning | N/A | N/A |
31
+ | CLAUDE.md | Skipped | N/A | N/A |
32
+
33
+ ### Files Changed in Revision
34
+
35
+ - `src/types/windsurf.ts` — `agentWorkflows` → `agentSkills: WindsurfGeneratedSkill[]`
36
+ - `src/converters/claude-to-windsurf.ts` — `convertAgentToSkill()`, updated content transforms
37
+ - `src/targets/windsurf.ts` — Skills written as `skills/{name}/SKILL.md`, flat workflows
38
+ - Tests updated to match
39
+
40
+ ---
41
+
42
+ ## Enhancement Summary
43
+
44
+ **Deepened on:** 2026-02-25
45
+ **Research agents used:** architecture-strategist, kieran-typescript-reviewer, security-sentinel, code-simplicity-reviewer, pattern-recognition-specialist
46
+ **External research:** Windsurf MCP docs, Windsurf tutorial docs
47
+
48
+ ### Key Improvements from Deepening
49
+ 1. **HTTP/SSE servers should be INCLUDED** — Windsurf supports all 3 transport types (stdio, Streamable HTTP, SSE). Original plan incorrectly skipped them.
50
+ 2. **File permissions: use `0o600`** — `mcp_config.json` contains secrets and must not be world-readable. Add secure write support.
51
+ 3. **Extract `resolveTargetOutputRoot` to shared utility** — both commands duplicate this; adding scope makes it worse. Extract first.
52
+ 4. **Bug fix: missing `result[name] = entry`** — all 5 review agents caught a copy-paste bug in the `buildMcpConfig` sample code.
53
+ 5. **`hasPotentialSecrets` to shared utility** — currently in sync.ts, would be duplicated. Extract to `src/utils/secrets.ts`.
54
+ 6. **Windsurf `mcp_config.json` is global-only** — per Windsurf docs, no per-project MCP config support. Workspace scope writes it for forward-compatibility but emit a warning.
55
+ 7. **Windsurf supports `${env:VAR}` interpolation** — consider writing env var references instead of literal values for secrets.
56
+
57
+ ### New Considerations Discovered
58
+ - Backup files accumulate with secrets and are never cleaned up — cap at 3 backups
59
+ - Workspace `mcp_config.json` could be committed to git — warn about `.gitignore`
60
+ - `WindsurfMcpServerEntry` type needs `serverUrl` field for HTTP/SSE servers
61
+ - Simplicity reviewer recommends handling scope as windsurf-specific in CLI rather than generic `TargetHandler` fields — but brainstorm explicitly chose "generic with windsurf as first adopter". **Decision: keep generic approach** per user's brainstorm decision, with JSDoc documenting the relationship between `defaultScope` and `supportedScopes`.
62
+
63
+ ---
64
+
65
+ ## Overview
66
+
67
+ Add a generic `--scope global|workspace` flag to the converter CLI with Windsurf as the first adopter. Global scope writes to `~/.codeium/windsurf/`, making workflows, skills, and MCP servers available across all projects. This also upgrades MCP handling from a human-readable setup doc (`mcp-setup.md`) to a proper machine-readable config (`mcp_config.json`), and removes AGENTS.md generation (the plugin's CLAUDE.md contains development-internal instructions, not user-facing content).
68
+
69
+ ## Problem Statement / Motivation
70
+
71
+ The current Windsurf converter (v0.10.0) writes everything to project-level `.windsurf/`, requiring re-installation per project. Windsurf supports global paths for skills (`~/.codeium/windsurf/skills/`) and MCP config (`~/.codeium/windsurf/mcp_config.json`). Users should install once and get capabilities everywhere.
72
+
73
+ Additionally, the v0.10.0 MCP output was a markdown setup guide — not an actual integration. Windsurf reads `mcp_config.json` directly, so we should write to that file.
74
+
75
+ ## Breaking Changes from v0.10.0
76
+
77
+ This is a **minor version bump** (v0.11.0) with intentional breaking changes to the experimental Windsurf target:
78
+
79
+ 1. **Default output location changed** — `--to windsurf` now defaults to global scope (`~/.codeium/windsurf/`). Use `--scope workspace` for the old behavior.
80
+ 2. **AGENTS.md no longer generated** — old files are left in place (not deleted).
81
+ 3. **`mcp-setup.md` replaced by `mcp_config.json`** — proper machine-readable integration. Old files left in place.
82
+ 4. **Env var secrets included with warning** — previously redacted, now included (required for the config file to work).
83
+ 5. **`--output` semantics changed** — `--output` now specifies the direct target directory (not a parent where `.windsurf/` is created).
84
+
85
+ ## Proposed Solution
86
+
87
+ ### Phase 0: Extract Shared Utilities (prerequisite)
88
+
89
+ **Files:** `src/utils/resolve-output.ts` (new), `src/utils/secrets.ts` (new)
90
+
91
+ #### 0a. Extract `resolveTargetOutputRoot` to shared utility
92
+
93
+ Both `install.ts` and `convert.ts` have near-identical `resolveTargetOutputRoot` functions that are already diverging (`hasExplicitOutput` exists in install.ts but not convert.ts). Adding scope would make the duplication worse.
94
+
95
+ - [x] Create `src/utils/resolve-output.ts` with a unified function:
96
+
97
+ ```typescript
98
+ import os from "os"
99
+ import path from "path"
100
+ import type { TargetScope } from "../targets"
101
+
102
+ export function resolveTargetOutputRoot(options: {
103
+ targetName: string
104
+ outputRoot: string
105
+ codexHome: string
106
+ piHome: string
107
+ hasExplicitOutput: boolean
108
+ scope?: TargetScope
109
+ }): string {
110
+ const { targetName, outputRoot, codexHome, piHome, hasExplicitOutput, scope } = options
111
+ if (targetName === "codex") return codexHome
112
+ if (targetName === "pi") return piHome
113
+ if (targetName === "droid") return path.join(os.homedir(), ".factory")
114
+ if (targetName === "cursor") {
115
+ const base = hasExplicitOutput ? outputRoot : process.cwd()
116
+ return path.join(base, ".cursor")
117
+ }
118
+ if (targetName === "gemini") {
119
+ const base = hasExplicitOutput ? outputRoot : process.cwd()
120
+ return path.join(base, ".gemini")
121
+ }
122
+ if (targetName === "copilot") {
123
+ const base = hasExplicitOutput ? outputRoot : process.cwd()
124
+ return path.join(base, ".github")
125
+ }
126
+ if (targetName === "kiro") {
127
+ const base = hasExplicitOutput ? outputRoot : process.cwd()
128
+ return path.join(base, ".kiro")
129
+ }
130
+ if (targetName === "windsurf") {
131
+ if (hasExplicitOutput) return outputRoot
132
+ if (scope === "global") return path.join(os.homedir(), ".codeium", "windsurf")
133
+ return path.join(process.cwd(), ".windsurf")
134
+ }
135
+ return outputRoot
136
+ }
137
+ ```
138
+
139
+ - [x] Update `install.ts` to import and call `resolveTargetOutputRoot` from shared utility
140
+ - [x] Update `convert.ts` to import and call `resolveTargetOutputRoot` from shared utility
141
+ - [x] Add `hasExplicitOutput` tracking to `convert.ts` (currently missing)
142
+
143
+ ### Research Insights (Phase 0)
144
+
145
+ **Architecture review:** Both commands will call the same function with the same signature. This eliminates the divergence and ensures scope resolution has a single source of truth. The `--also` loop in both commands also uses this function with `handler.defaultScope`.
146
+
147
+ **Pattern review:** This follows the same extraction pattern as `resolveTargetHome` in `src/utils/resolve-home.ts`.
148
+
149
+ #### 0b. Extract `hasPotentialSecrets` to shared utility
150
+
151
+ Currently in `sync.ts:20-31`. The same regex pattern also appears in `claude-to-windsurf.ts:223` as `redactEnvValue`. Extract to avoid a third copy.
152
+
153
+ - [x] Create `src/utils/secrets.ts`:
154
+
155
+ ```typescript
156
+ const SENSITIVE_PATTERN = /key|token|secret|password|credential|api_key/i
157
+
158
+ export function hasPotentialSecrets(
159
+ servers: Record<string, { env?: Record<string, string> }>,
160
+ ): boolean {
161
+ for (const server of Object.values(servers)) {
162
+ if (server.env) {
163
+ for (const key of Object.keys(server.env)) {
164
+ if (SENSITIVE_PATTERN.test(key)) return true
165
+ }
166
+ }
167
+ }
168
+ return false
169
+ }
170
+ ```
171
+
172
+ - [x] Update `sync.ts` to import from shared utility
173
+ - [x] Use in new windsurf converter
174
+
175
+ ### Phase 1: Types and TargetHandler
176
+
177
+ **Files:** `src/types/windsurf.ts`, `src/targets/index.ts`
178
+
179
+ #### 1a. Update WindsurfBundle type
180
+
181
+ ```typescript
182
+ // src/types/windsurf.ts
183
+ export type WindsurfMcpServerEntry = {
184
+ command?: string
185
+ args?: string[]
186
+ env?: Record<string, string>
187
+ serverUrl?: string
188
+ headers?: Record<string, string>
189
+ }
190
+
191
+ export type WindsurfMcpConfig = {
192
+ mcpServers: Record<string, WindsurfMcpServerEntry>
193
+ }
194
+
195
+ export type WindsurfBundle = {
196
+ agentWorkflows: WindsurfWorkflow[]
197
+ commandWorkflows: WindsurfWorkflow[]
198
+ skillDirs: WindsurfSkillDir[]
199
+ mcpConfig: WindsurfMcpConfig | null
200
+ }
201
+ ```
202
+
203
+ - [x] Remove `agentsMd: string | null`
204
+ - [x] Replace `mcpSetupDoc: string | null` with `mcpConfig: WindsurfMcpConfig | null`
205
+ - [x] Add `WindsurfMcpServerEntry` (supports both stdio and HTTP/SSE) and `WindsurfMcpConfig` types
206
+
207
+ ### Research Insights (Phase 1a)
208
+
209
+ **Windsurf docs confirm** three transport types: stdio (`command` + `args`), Streamable HTTP (`serverUrl`), and SSE (`serverUrl` or `url`). The `WindsurfMcpServerEntry` type must support all three — making `command` optional and adding `serverUrl` and `headers` fields.
210
+
211
+ **TypeScript reviewer:** Consider making `WindsurfMcpServerEntry` a discriminated union if strict typing is desired. However, since this mirrors JSON config structure, a flat type with optional fields is pragmatically simpler.
212
+
213
+ #### 1b. Add TargetScope to TargetHandler
214
+
215
+ ```typescript
216
+ // src/targets/index.ts
217
+ export type TargetScope = "global" | "workspace"
218
+
219
+ export type TargetHandler<TBundle = unknown> = {
220
+ name: string
221
+ implemented: boolean
222
+ /**
223
+ * Default scope when --scope is not provided.
224
+ * Only meaningful when supportedScopes is defined.
225
+ * Falls back to "workspace" if absent.
226
+ */
227
+ defaultScope?: TargetScope
228
+ /** Valid scope values. If absent, the --scope flag is rejected for this target. */
229
+ supportedScopes?: TargetScope[]
230
+ convert: (plugin: ClaudePlugin, options: ClaudeToOpenCodeOptions) => TBundle | null
231
+ write: (outputRoot: string, bundle: TBundle) => Promise<void>
232
+ }
233
+ ```
234
+
235
+ - [x] Add `TargetScope` type export
236
+ - [x] Add `defaultScope?` and `supportedScopes?` to `TargetHandler` with JSDoc
237
+ - [x] Set windsurf target: `defaultScope: "global"`, `supportedScopes: ["global", "workspace"]`
238
+ - [x] No changes to other targets (they have no scope fields, flag is ignored)
239
+
240
+ ### Research Insights (Phase 1b)
241
+
242
+ **Simplicity review:** Argued this is premature generalization (only 1 of 8 targets uses scopes). Recommended handling scope as windsurf-specific with `if (targetName !== "windsurf")` guard instead. **Decision: keep generic approach** per brainstorm decision "Generic with windsurf as first adopter", but add JSDoc documenting the invariant.
243
+
244
+ **TypeScript review:** Suggested a `ScopeConfig` grouped object to prevent `defaultScope` without `supportedScopes`. The JSDoc approach is simpler and sufficient for now.
245
+
246
+ **Architecture review:** Adding optional fields to `TargetHandler` follows Open/Closed Principle — existing targets are unaffected. Clean extension.
247
+
248
+ ### Phase 2: Converter Changes
249
+
250
+ **Files:** `src/converters/claude-to-windsurf.ts`
251
+
252
+ #### 2a. Remove AGENTS.md generation
253
+
254
+ - [x] Remove `buildAgentsMd()` function
255
+ - [x] Remove `agentsMd` from return value
256
+
257
+ #### 2b. Replace MCP setup doc with MCP config
258
+
259
+ - [x] Remove `buildMcpSetupDoc()` function
260
+ - [x] Remove `redactEnvValue()` helper
261
+ - [x] Add `buildMcpConfig()` that returns `WindsurfMcpConfig | null`
262
+ - [x] Include **all** env vars (including secrets) — no redaction
263
+ - [x] Use shared `hasPotentialSecrets()` from `src/utils/secrets.ts`
264
+ - [x] Include **both** stdio and HTTP/SSE servers (Windsurf supports all transport types)
265
+
266
+ ```typescript
267
+ function buildMcpConfig(
268
+ servers?: Record<string, ClaudeMcpServer>,
269
+ ): WindsurfMcpConfig | null {
270
+ if (!servers || Object.keys(servers).length === 0) return null
271
+
272
+ const result: Record<string, WindsurfMcpServerEntry> = {}
273
+ for (const [name, server] of Object.entries(servers)) {
274
+ if (server.command) {
275
+ // stdio transport
276
+ const entry: WindsurfMcpServerEntry = { command: server.command }
277
+ if (server.args?.length) entry.args = server.args
278
+ if (server.env && Object.keys(server.env).length > 0) entry.env = server.env
279
+ result[name] = entry
280
+ } else if (server.url) {
281
+ // HTTP/SSE transport
282
+ const entry: WindsurfMcpServerEntry = { serverUrl: server.url }
283
+ if (server.headers && Object.keys(server.headers).length > 0) entry.headers = server.headers
284
+ if (server.env && Object.keys(server.env).length > 0) entry.env = server.env
285
+ result[name] = entry
286
+ } else {
287
+ console.warn(`Warning: MCP server "${name}" has no command or URL. Skipping.`)
288
+ continue
289
+ }
290
+ }
291
+
292
+ if (Object.keys(result).length === 0) return null
293
+
294
+ // Warn about secrets (don't redact — they're needed for the config to work)
295
+ if (hasPotentialSecrets(result)) {
296
+ console.warn(
297
+ "Warning: MCP servers contain env vars that may include secrets (API keys, tokens).\n" +
298
+ " These will be written to mcp_config.json. Review before sharing the config file.",
299
+ )
300
+ }
301
+
302
+ return { mcpServers: result }
303
+ }
304
+ ```
305
+
306
+ ### Research Insights (Phase 2)
307
+
308
+ **Windsurf docs (critical correction):** Windsurf supports **stdio, Streamable HTTP, and SSE** transports in `mcp_config.json`. HTTP/SSE servers use `serverUrl` (not `url`). The original plan incorrectly planned to skip HTTP/SSE servers. This is now corrected — all transport types are included.
309
+
310
+ **All 5 review agents flagged:** The original code sample was missing `result[name] = entry` — the entry was built but never stored. Fixed above.
311
+
312
+ **Security review:** The warning message should enumerate which specific env var names triggered detection. Enhanced version:
313
+
314
+ ```typescript
315
+ if (hasPotentialSecrets(result)) {
316
+ const flagged = Object.entries(result)
317
+ .filter(([, s]) => s.env && Object.keys(s.env).some(k => SENSITIVE_PATTERN.test(k)))
318
+ .map(([name]) => name)
319
+ console.warn(
320
+ `Warning: MCP servers contain env vars that may include secrets: ${flagged.join(", ")}.\n` +
321
+ " These will be written to mcp_config.json. Review before sharing the config file.",
322
+ )
323
+ }
324
+ ```
325
+
326
+ **Windsurf env var interpolation:** Windsurf supports `${env:VARIABLE_NAME}` syntax in `mcp_config.json`. Future enhancement: write env var references instead of literal values for secrets. Out of scope for v0.11.0 (requires more research on which fields support interpolation).
327
+
328
+ ### Phase 3: Writer Changes
329
+
330
+ **Files:** `src/targets/windsurf.ts`, `src/utils/files.ts`
331
+
332
+ #### 3a. Simplify writer — remove AGENTS.md and double-nesting guard
333
+
334
+ The writer always writes directly into `outputRoot`. The CLI resolves the correct output root based on scope.
335
+
336
+ - [x] Remove AGENTS.md writing block (lines 10-17)
337
+ - [x] Remove `resolveWindsurfPaths()` — no longer needed
338
+ - [x] Write workflows, skills, and MCP config directly into `outputRoot`
339
+
340
+ ### Research Insights (Phase 3a)
341
+
342
+ **Pattern review (dissent):** Every other writer (kiro, copilot, gemini, droid) has a `resolve*Paths()` function with a double-nesting guard. Removing it makes Windsurf the only target where the CLI fully owns nesting. This creates an inconsistency in the `write()` contract.
343
+
344
+ **Resolution:** Accept the divergence — Windsurf has genuinely different semantics (global vs workspace). Add a JSDoc comment on `TargetHandler.write()` documenting that some writers may apply additional nesting while the Windsurf writer expects the final resolved path. Long-term, other targets could migrate to this pattern in a separate refactor.
345
+
346
+ #### 3b. Replace MCP setup doc with JSON config merge
347
+
348
+ Follow Kiro pattern (`src/targets/kiro.ts:68-92`) with security hardening:
349
+
350
+ - [x] Read existing `mcp_config.json` if present
351
+ - [x] Backup before overwrite (`backupFile()`)
352
+ - [x] Parse existing JSON (warn and replace if corrupted; add `!Array.isArray()` guard)
353
+ - [x] Merge at `mcpServers` key: plugin entries overwrite same-name entries, user entries preserved
354
+ - [x] Preserve all other top-level keys in existing file
355
+ - [x] Write merged result with **restrictive permissions** (`0o600`)
356
+ - [x] Emit warning when writing to workspace scope (Windsurf `mcp_config.json` is global-only per docs)
357
+
358
+ ```typescript
359
+ // MCP config merge with security hardening
360
+ if (bundle.mcpConfig) {
361
+ const mcpPath = path.join(outputRoot, "mcp_config.json")
362
+ const backupPath = await backupFile(mcpPath)
363
+ if (backupPath) {
364
+ console.log(`Backed up existing mcp_config.json to ${backupPath}`)
365
+ }
366
+
367
+ let existingConfig: Record<string, unknown> = {}
368
+ if (await pathExists(mcpPath)) {
369
+ try {
370
+ const parsed = await readJson<unknown>(mcpPath)
371
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
372
+ existingConfig = parsed as Record<string, unknown>
373
+ }
374
+ } catch {
375
+ console.warn("Warning: existing mcp_config.json could not be parsed and will be replaced.")
376
+ }
377
+ }
378
+
379
+ const existingServers =
380
+ existingConfig.mcpServers &&
381
+ typeof existingConfig.mcpServers === "object" &&
382
+ !Array.isArray(existingConfig.mcpServers)
383
+ ? (existingConfig.mcpServers as Record<string, unknown>)
384
+ : {}
385
+ const merged = { ...existingConfig, mcpServers: { ...existingServers, ...bundle.mcpConfig.mcpServers } }
386
+ await writeJsonSecure(mcpPath, merged) // 0o600 permissions
387
+ }
388
+ ```
389
+
390
+ ### Research Insights (Phase 3b)
391
+
392
+ **Security review (HIGH):** The current `writeJson()` in `src/utils/files.ts` uses default umask (`0o644`) — world-readable. The sync targets all use `{ mode: 0o600 }` for secret-containing files. The Windsurf writer (and Kiro writer) must do the same.
393
+
394
+ **Implementation:** Add a `writeJsonSecure()` helper or add a `mode` parameter to `writeJson()`:
395
+
396
+ ```typescript
397
+ // src/utils/files.ts
398
+ export async function writeJsonSecure(filePath: string, data: unknown): Promise<void> {
399
+ const content = JSON.stringify(data, null, 2)
400
+ await ensureDir(path.dirname(filePath))
401
+ await fs.writeFile(filePath, content + "\n", { encoding: "utf8", mode: 0o600 })
402
+ }
403
+ ```
404
+
405
+ **Security review (MEDIUM):** Backup files inherit default permissions. Ensure `backupFile()` also sets `0o600` on the backup copy when the source may contain secrets.
406
+
407
+ **Security review (MEDIUM):** Workspace `mcp_config.json` could be committed to git. After writing to workspace scope, emit a warning:
408
+
409
+ ```
410
+ Warning: .windsurf/mcp_config.json may contain secrets. Ensure it is in .gitignore.
411
+ ```
412
+
413
+ **TypeScript review:** The `readJson<Record<string, unknown>>` assertion is unsafe — a valid JSON array or string passes parsing but fails the type. Added `!Array.isArray()` guard.
414
+
415
+ **TypeScript review:** The `bundle.mcpConfig` null check is sufficient — when non-null, `mcpServers` is guaranteed to have entries (the converter returns null for empty servers). Simplified from `bundle.mcpConfig && Object.keys(...)`.
416
+
417
+ **Windsurf docs (important):** `mcp_config.json` is a **global configuration only** — Windsurf has no per-project MCP config support. Writing it to `.windsurf/` in workspace scope may not be discovered by Windsurf. Emit a warning for workspace scope but still write the file for forward-compatibility.
418
+
419
+ #### 3c. Updated writer structure
420
+
421
+ ```typescript
422
+ export async function writeWindsurfBundle(outputRoot: string, bundle: WindsurfBundle): Promise<void> {
423
+ await ensureDir(outputRoot)
424
+
425
+ // Write agent workflows
426
+ if (bundle.agentWorkflows.length > 0) {
427
+ const agentDir = path.join(outputRoot, "workflows", "agents")
428
+ await ensureDir(agentDir)
429
+ for (const workflow of bundle.agentWorkflows) {
430
+ validatePathSafe(workflow.name, "agent workflow")
431
+ const content = formatFrontmatter({ description: workflow.description }, `# ${workflow.name}\n\n${workflow.body}`)
432
+ await writeText(path.join(agentDir, `${workflow.name}.md`), content + "\n")
433
+ }
434
+ }
435
+
436
+ // Write command workflows
437
+ if (bundle.commandWorkflows.length > 0) {
438
+ const cmdDir = path.join(outputRoot, "workflows", "commands")
439
+ await ensureDir(cmdDir)
440
+ for (const workflow of bundle.commandWorkflows) {
441
+ validatePathSafe(workflow.name, "command workflow")
442
+ const content = formatFrontmatter({ description: workflow.description }, `# ${workflow.name}\n\n${workflow.body}`)
443
+ await writeText(path.join(cmdDir, `${workflow.name}.md`), content + "\n")
444
+ }
445
+ }
446
+
447
+ // Copy skill directories
448
+ if (bundle.skillDirs.length > 0) {
449
+ const skillsDir = path.join(outputRoot, "skills")
450
+ await ensureDir(skillsDir)
451
+ for (const skill of bundle.skillDirs) {
452
+ validatePathSafe(skill.name, "skill directory")
453
+ const destDir = path.join(skillsDir, skill.name)
454
+ const resolvedDest = path.resolve(destDir)
455
+ if (!resolvedDest.startsWith(path.resolve(skillsDir))) {
456
+ console.warn(`Warning: Skill name "${skill.name}" escapes skills/. Skipping.`)
457
+ continue
458
+ }
459
+ await copyDir(skill.sourceDir, destDir)
460
+ }
461
+ }
462
+
463
+ // Merge MCP config (see 3b above)
464
+ if (bundle.mcpConfig) {
465
+ // ... merge logic from 3b
466
+ }
467
+ }
468
+ ```
469
+
470
+ ### Phase 4: CLI Wiring
471
+
472
+ **Files:** `src/commands/install.ts`, `src/commands/convert.ts`
473
+
474
+ #### 4a. Add `--scope` flag to both commands
475
+
476
+ ```typescript
477
+ scope: {
478
+ type: "string",
479
+ description: "Scope level: global | workspace (default varies by target)",
480
+ },
481
+ ```
482
+
483
+ - [x] Add `scope` arg to `install.ts`
484
+ - [x] Add `scope` arg to `convert.ts`
485
+
486
+ #### 4b. Validate scope with type guard
487
+
488
+ Use a proper type guard instead of unsafe `as TargetScope` cast:
489
+
490
+ ```typescript
491
+ function isTargetScope(value: string): value is TargetScope {
492
+ return value === "global" || value === "workspace"
493
+ }
494
+
495
+ const scopeValue = args.scope ? String(args.scope) : undefined
496
+ if (scopeValue !== undefined) {
497
+ if (!target.supportedScopes) {
498
+ throw new Error(`Target "${targetName}" does not support the --scope flag.`)
499
+ }
500
+ if (!isTargetScope(scopeValue) || !target.supportedScopes.includes(scopeValue)) {
501
+ throw new Error(`Target "${targetName}" does not support --scope ${scopeValue}. Supported: ${target.supportedScopes.join(", ")}`)
502
+ }
503
+ }
504
+ const resolvedScope = scopeValue ?? target.defaultScope ?? "workspace"
505
+ ```
506
+
507
+ - [x] Add `isTargetScope` type guard
508
+ - [x] Add scope validation in both commands (single block, not two separate checks)
509
+
510
+ ### Research Insights (Phase 4b)
511
+
512
+ **TypeScript review:** The original plan cast `scopeValue as TargetScope` before validation — a type lie. Use a proper type guard function to keep the type system honest.
513
+
514
+ **Simplicity review:** The two-step validation (check supported, then check exists) can be a single block with the type guard approach above.
515
+
516
+ #### 4c. Update output root resolution
517
+
518
+ Both commands now use the shared `resolveTargetOutputRoot` from Phase 0a.
519
+
520
+ - [x] Call shared function with `scope: resolvedScope` for primary target
521
+ - [x] Default scope: `target.defaultScope ?? "workspace"` (only used when target supports scopes)
522
+
523
+ #### 4d. Handle `--also` targets
524
+
525
+ `--scope` applies only to the primary `--to` target. Extra `--also` targets use their own `defaultScope`.
526
+
527
+ - [x] Pass `handler.defaultScope` for `--also` targets (each uses its own default)
528
+ - [x] Update the `--also` loop in both commands to use target-specific scope resolution
529
+
530
+ ### Research Insights (Phase 4d)
531
+
532
+ **Architecture review:** There is no way for users to specify scope for an `--also` target (e.g., `--also windsurf:workspace`). Accept as a known v0.11.0 limitation. If users need workspace scope for windsurf, they can run two separate commands. Add a code comment indicating where per-target scope overrides would be added in the future.
533
+
534
+ ### Phase 5: Tests
535
+
536
+ **Files:** `tests/windsurf-converter.test.ts`, `tests/windsurf-writer.test.ts`
537
+
538
+ #### 5a. Update converter tests
539
+
540
+ - [x] Remove all AGENTS.md tests (lines 275-303: empty plugin, CLAUDE.md missing)
541
+ - [x] Remove all `mcpSetupDoc` tests (lines 305-366: stdio, HTTP/SSE, redaction, null)
542
+ - [x] Update `fixturePlugin` default — remove `agentsMd` and `mcpSetupDoc` references
543
+ - [x] Add `mcpConfig` tests:
544
+ - stdio server produces correct JSON structure with `command`, `args`, `env`
545
+ - HTTP/SSE server produces correct JSON structure with `serverUrl`, `headers`
546
+ - mixed servers (stdio + HTTP) both included
547
+ - env vars included (not redacted) — verify actual values present
548
+ - `hasPotentialSecrets()` emits console.warn for sensitive keys
549
+ - `hasPotentialSecrets()` does NOT warn when no sensitive keys
550
+ - no servers produces null mcpConfig
551
+ - empty bundle has null mcpConfig
552
+ - server with no command and no URL is skipped with warning
553
+
554
+ #### 5b. Update writer tests
555
+
556
+ - [x] Remove AGENTS.md tests (backup test, creation test, double-nesting AGENTS.md parent test)
557
+ - [x] Remove double-nesting guard test (guard removed)
558
+ - [x] Remove `mcp-setup.md` write test
559
+ - [x] Update `emptyBundle` fixture — remove `agentsMd`, `mcpSetupDoc`, add `mcpConfig: null`
560
+ - [x] Add `mcp_config.json` tests:
561
+ - writes mcp_config.json to outputRoot
562
+ - merges with existing mcp_config.json (preserves user servers)
563
+ - backs up existing mcp_config.json before overwrite
564
+ - handles corrupted existing mcp_config.json (warn and replace)
565
+ - handles existing mcp_config.json with array (not object) at root
566
+ - handles existing mcp_config.json with `mcpServers: null`
567
+ - preserves non-mcpServers keys in existing file
568
+ - server name collision: plugin entry wins
569
+ - file permissions are 0o600 (not world-readable)
570
+ - [x] Update full bundle test — writer writes directly into outputRoot (no `.windsurf/` nesting)
571
+
572
+ #### 5c. Add scope resolution tests
573
+
574
+ Test the shared `resolveTargetOutputRoot` function:
575
+
576
+ - [x] Default scope for windsurf is "global" → resolves to `~/.codeium/windsurf/`
577
+ - [x] Explicit `--scope workspace` → resolves to `cwd/.windsurf/`
578
+ - [x] `--output` overrides scope resolution (both global and workspace)
579
+ - [x] Invalid scope value for windsurf → error
580
+ - [x] `--scope` on non-scope target (e.g., opencode) → error
581
+ - [x] `--also windsurf` uses windsurf's default scope ("global")
582
+ - [x] `isTargetScope` type guard correctly identifies valid/invalid values
583
+
584
+ ### Phase 6: Documentation
585
+
586
+ **Files:** `README.md`, `CHANGELOG.md`
587
+
588
+ - [x] Update README.md Windsurf section to mention `--scope` flag and global default
589
+ - [x] Add CHANGELOG entry for v0.11.0 with breaking changes documented
590
+ - [x] Document migration path: `--scope workspace` for old behavior
591
+ - [x] Note that Windsurf `mcp_config.json` is global-only (workspace MCP config may not be discovered)
592
+
593
+ ## Acceptance Criteria
594
+
595
+ - [x] `install compound-engineering --to windsurf` writes to `~/.codeium/windsurf/` by default
596
+ - [x] `install compound-engineering --to windsurf --scope workspace` writes to `cwd/.windsurf/`
597
+ - [x] `--output /custom/path` overrides scope for both commands
598
+ - [x] `--scope` on non-supporting target produces clear error
599
+ - [x] `mcp_config.json` merges with existing file (backup created, user entries preserved)
600
+ - [x] `mcp_config.json` written with `0o600` permissions (not world-readable)
601
+ - [x] No AGENTS.md generated for either scope
602
+ - [x] Env var secrets included in `mcp_config.json` with `console.warn` listing affected servers
603
+ - [x] Both stdio and HTTP/SSE MCP servers included in `mcp_config.json`
604
+ - [x] All existing tests updated, all new tests pass
605
+ - [x] No regressions in other targets
606
+ - [x] `resolveTargetOutputRoot` extracted to shared utility (no duplication)
607
+
608
+ ## Dependencies & Risks
609
+
610
+ **Risk: Global workflow path is undocumented.** Windsurf may not discover workflows from `~/.codeium/windsurf/workflows/`. Mitigation: documented as a known assumption in the brainstorm. Users can `--scope workspace` if global workflows aren't discovered.
611
+
612
+ **Risk: Breaking changes for existing v0.10.0 users.** Mitigation: document migration path clearly. `--scope workspace` restores previous behavior. Target is experimental with a small user base.
613
+
614
+ **Risk: Workspace `mcp_config.json` not read by Windsurf.** Per Windsurf docs, `mcp_config.json` is global-only configuration. Workspace scope writes the file for forward-compatibility but emits a warning. The primary use case is global scope anyway.
615
+
616
+ **Risk: Secrets in `mcp_config.json` committed to git.** Mitigation: `0o600` file permissions, console.warn about sensitive env vars, warning about `.gitignore` for workspace scope.
617
+
618
+ ## References & Research
619
+
620
+ - Spec: `docs/specs/windsurf.md` (authoritative reference for component mapping)
621
+ - Kiro MCP merge pattern: [src/targets/kiro.ts:68-92](../../src/targets/kiro.ts)
622
+ - Sync secrets warning: [src/commands/sync.ts:20-28](../../src/commands/sync.ts)
623
+ - Windsurf MCP docs: https://docs.windsurf.com/windsurf/cascade/mcp
624
+ - Windsurf Skills global path: https://docs.windsurf.com/windsurf/cascade/skills
625
+ - Windsurf MCP tutorial: https://windsurf.com/university/tutorials/configuring-first-mcp-server
626
+ - Adding converter targets (learning): [docs/solutions/adding-converter-target-providers.md](../solutions/adding-converter-target-providers.md)
627
+ - Plugin versioning (learning): [docs/solutions/plugin-versioning-requirements.md](../solutions/plugin-versioning-requirements.md)