Files
claude-engineering-plugin/docs/plans/2026-02-25-feat-windsurf-global-scope-support-plan.md
Ryan Burnham 1107e3dd31 docs: update spec and plan to reflect global_workflows/ directory
Updated docs/specs/windsurf.md and the plan to accurately document
that global scope workflows go in global_workflows/ while workspace
scope workflows go in workflows/.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 21:02:25 +08:00

30 KiB

title, type, status, date, deepened, prior
title type status date deepened prior
Windsurf Global Scope Support feat completed 2026-02-25 2026-02-25 docs/plans/2026-02-23-feat-add-windsurf-target-provider-plan.md (removed — superseded)

Windsurf Global Scope Support

Post-Implementation Revisions (2026-02-26)

After auditing the implementation against docs/specs/windsurf.md, two significant changes were made:

  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).

  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.

  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.

Final Component Mapping (per spec)

Claude Code Windsurf Output Path Invocation
Agents (.md) Skills skills/{name}/SKILL.md @skill-name or automatic
Commands (.md) Workflows (flat) global_workflows/{name}.md (global) / workflows/{name}.md (workspace) /{workflow-name}
Skills (SKILL.md) Skills (pass-through) skills/{name}/SKILL.md @skill-name
MCP servers mcp_config.json mcp_config.json N/A
Hooks Skipped with warning N/A N/A
CLAUDE.md Skipped N/A N/A

Files Changed in Revision

  • src/types/windsurf.tsagentWorkflowsagentSkills: WindsurfGeneratedSkill[]
  • src/converters/claude-to-windsurf.tsconvertAgentToSkill(), updated content transforms
  • src/targets/windsurf.ts — Skills written as skills/{name}/SKILL.md, flat workflows
  • Tests updated to match

Enhancement Summary

Deepened on: 2026-02-25 Research agents used: architecture-strategist, kieran-typescript-reviewer, security-sentinel, code-simplicity-reviewer, pattern-recognition-specialist External research: Windsurf MCP docs, Windsurf tutorial docs

Key Improvements from Deepening

  1. HTTP/SSE servers should be INCLUDED — Windsurf supports all 3 transport types (stdio, Streamable HTTP, SSE). Original plan incorrectly skipped them.
  2. File permissions: use 0o600mcp_config.json contains secrets and must not be world-readable. Add secure write support.
  3. Extract resolveTargetOutputRoot to shared utility — both commands duplicate this; adding scope makes it worse. Extract first.
  4. Bug fix: missing result[name] = entry — all 5 review agents caught a copy-paste bug in the buildMcpConfig sample code.
  5. hasPotentialSecrets to shared utility — currently in sync.ts, would be duplicated. Extract to src/utils/secrets.ts.
  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.
  7. Windsurf supports ${env:VAR} interpolation — consider writing env var references instead of literal values for secrets.

New Considerations Discovered

  • Backup files accumulate with secrets and are never cleaned up — cap at 3 backups
  • Workspace mcp_config.json could be committed to git — warn about .gitignore
  • WindsurfMcpServerEntry type needs serverUrl field for HTTP/SSE servers
  • 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.

Overview

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).

Problem Statement / Motivation

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.

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.

Breaking Changes from v0.10.0

This is a minor version bump (v0.11.0) with intentional breaking changes to the experimental Windsurf target:

  1. Default output location changed--to windsurf now defaults to global scope (~/.codeium/windsurf/). Use --scope workspace for the old behavior.
  2. AGENTS.md no longer generated — old files are left in place (not deleted).
  3. mcp-setup.md replaced by mcp_config.json — proper machine-readable integration. Old files left in place.
  4. Env var secrets included with warning — previously redacted, now included (required for the config file to work).
  5. --output semantics changed--output now specifies the direct target directory (not a parent where .windsurf/ is created).

Proposed Solution

Phase 0: Extract Shared Utilities (prerequisite)

Files: src/utils/resolve-output.ts (new), src/utils/secrets.ts (new)

0a. Extract resolveTargetOutputRoot to shared utility

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.

  • Create src/utils/resolve-output.ts with a unified function:
import os from "os"
import path from "path"
import type { TargetScope } from "../targets"

export function resolveTargetOutputRoot(options: {
  targetName: string
  outputRoot: string
  codexHome: string
  piHome: string
  hasExplicitOutput: boolean
  scope?: TargetScope
}): string {
  const { targetName, outputRoot, codexHome, piHome, hasExplicitOutput, scope } = options
  if (targetName === "codex") return codexHome
  if (targetName === "pi") return piHome
  if (targetName === "droid") return path.join(os.homedir(), ".factory")
  if (targetName === "cursor") {
    const base = hasExplicitOutput ? outputRoot : process.cwd()
    return path.join(base, ".cursor")
  }
  if (targetName === "gemini") {
    const base = hasExplicitOutput ? outputRoot : process.cwd()
    return path.join(base, ".gemini")
  }
  if (targetName === "copilot") {
    const base = hasExplicitOutput ? outputRoot : process.cwd()
    return path.join(base, ".github")
  }
  if (targetName === "kiro") {
    const base = hasExplicitOutput ? outputRoot : process.cwd()
    return path.join(base, ".kiro")
  }
  if (targetName === "windsurf") {
    if (hasExplicitOutput) return outputRoot
    if (scope === "global") return path.join(os.homedir(), ".codeium", "windsurf")
    return path.join(process.cwd(), ".windsurf")
  }
  return outputRoot
}
  • Update install.ts to import and call resolveTargetOutputRoot from shared utility
  • Update convert.ts to import and call resolveTargetOutputRoot from shared utility
  • Add hasExplicitOutput tracking to convert.ts (currently missing)

Research Insights (Phase 0)

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.

Pattern review: This follows the same extraction pattern as resolveTargetHome in src/utils/resolve-home.ts.

0b. Extract hasPotentialSecrets to shared utility

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.

  • Create src/utils/secrets.ts:
const SENSITIVE_PATTERN = /key|token|secret|password|credential|api_key/i

export function hasPotentialSecrets(
  servers: Record<string, { env?: Record<string, string> }>,
): boolean {
  for (const server of Object.values(servers)) {
    if (server.env) {
      for (const key of Object.keys(server.env)) {
        if (SENSITIVE_PATTERN.test(key)) return true
      }
    }
  }
  return false
}
  • Update sync.ts to import from shared utility
  • Use in new windsurf converter

Phase 1: Types and TargetHandler

Files: src/types/windsurf.ts, src/targets/index.ts

1a. Update WindsurfBundle type

// src/types/windsurf.ts
export type WindsurfMcpServerEntry = {
  command?: string
  args?: string[]
  env?: Record<string, string>
  serverUrl?: string
  headers?: Record<string, string>
}

export type WindsurfMcpConfig = {
  mcpServers: Record<string, WindsurfMcpServerEntry>
}

export type WindsurfBundle = {
  agentWorkflows: WindsurfWorkflow[]
  commandWorkflows: WindsurfWorkflow[]
  skillDirs: WindsurfSkillDir[]
  mcpConfig: WindsurfMcpConfig | null
}
  • Remove agentsMd: string | null
  • Replace mcpSetupDoc: string | null with mcpConfig: WindsurfMcpConfig | null
  • Add WindsurfMcpServerEntry (supports both stdio and HTTP/SSE) and WindsurfMcpConfig types

Research Insights (Phase 1a)

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.

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.

1b. Add TargetScope to TargetHandler

// src/targets/index.ts
export type TargetScope = "global" | "workspace"

export type TargetHandler<TBundle = unknown> = {
  name: string
  implemented: boolean
  /**
   * Default scope when --scope is not provided.
   * Only meaningful when supportedScopes is defined.
   * Falls back to "workspace" if absent.
   */
  defaultScope?: TargetScope
  /** Valid scope values. If absent, the --scope flag is rejected for this target. */
  supportedScopes?: TargetScope[]
  convert: (plugin: ClaudePlugin, options: ClaudeToOpenCodeOptions) => TBundle | null
  write: (outputRoot: string, bundle: TBundle) => Promise<void>
}
  • Add TargetScope type export
  • Add defaultScope? and supportedScopes? to TargetHandler with JSDoc
  • Set windsurf target: defaultScope: "global", supportedScopes: ["global", "workspace"]
  • No changes to other targets (they have no scope fields, flag is ignored)

Research Insights (Phase 1b)

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.

TypeScript review: Suggested a ScopeConfig grouped object to prevent defaultScope without supportedScopes. The JSDoc approach is simpler and sufficient for now.

Architecture review: Adding optional fields to TargetHandler follows Open/Closed Principle — existing targets are unaffected. Clean extension.

Phase 2: Converter Changes

Files: src/converters/claude-to-windsurf.ts

2a. Remove AGENTS.md generation

  • Remove buildAgentsMd() function
  • Remove agentsMd from return value

2b. Replace MCP setup doc with MCP config

  • Remove buildMcpSetupDoc() function
  • Remove redactEnvValue() helper
  • Add buildMcpConfig() that returns WindsurfMcpConfig | null
  • Include all env vars (including secrets) — no redaction
  • Use shared hasPotentialSecrets() from src/utils/secrets.ts
  • Include both stdio and HTTP/SSE servers (Windsurf supports all transport types)
function buildMcpConfig(
  servers?: Record<string, ClaudeMcpServer>,
): WindsurfMcpConfig | null {
  if (!servers || Object.keys(servers).length === 0) return null

  const result: Record<string, WindsurfMcpServerEntry> = {}
  for (const [name, server] of Object.entries(servers)) {
    if (server.command) {
      // stdio transport
      const entry: WindsurfMcpServerEntry = { command: server.command }
      if (server.args?.length) entry.args = server.args
      if (server.env && Object.keys(server.env).length > 0) entry.env = server.env
      result[name] = entry
    } else if (server.url) {
      // HTTP/SSE transport
      const entry: WindsurfMcpServerEntry = { serverUrl: server.url }
      if (server.headers && Object.keys(server.headers).length > 0) entry.headers = server.headers
      if (server.env && Object.keys(server.env).length > 0) entry.env = server.env
      result[name] = entry
    } else {
      console.warn(`Warning: MCP server "${name}" has no command or URL. Skipping.`)
      continue
    }
  }

  if (Object.keys(result).length === 0) return null

  // Warn about secrets (don't redact — they're needed for the config to work)
  if (hasPotentialSecrets(result)) {
    console.warn(
      "Warning: MCP servers contain env vars that may include secrets (API keys, tokens).\n" +
      "   These will be written to mcp_config.json. Review before sharing the config file.",
    )
  }

  return { mcpServers: result }
}

Research Insights (Phase 2)

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.

All 5 review agents flagged: The original code sample was missing result[name] = entry — the entry was built but never stored. Fixed above.

Security review: The warning message should enumerate which specific env var names triggered detection. Enhanced version:

if (hasPotentialSecrets(result)) {
  const flagged = Object.entries(result)
    .filter(([, s]) => s.env && Object.keys(s.env).some(k => SENSITIVE_PATTERN.test(k)))
    .map(([name]) => name)
  console.warn(
    `Warning: MCP servers contain env vars that may include secrets: ${flagged.join(", ")}.\n` +
    "   These will be written to mcp_config.json. Review before sharing the config file.",
  )
}

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).

Phase 3: Writer Changes

Files: src/targets/windsurf.ts, src/utils/files.ts

3a. Simplify writer — remove AGENTS.md and double-nesting guard

The writer always writes directly into outputRoot. The CLI resolves the correct output root based on scope.

  • Remove AGENTS.md writing block (lines 10-17)
  • Remove resolveWindsurfPaths() — no longer needed
  • Write workflows, skills, and MCP config directly into outputRoot

Research Insights (Phase 3a)

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.

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.

3b. Replace MCP setup doc with JSON config merge

Follow Kiro pattern (src/targets/kiro.ts:68-92) with security hardening:

  • Read existing mcp_config.json if present
  • Backup before overwrite (backupFile())
  • Parse existing JSON (warn and replace if corrupted; add !Array.isArray() guard)
  • Merge at mcpServers key: plugin entries overwrite same-name entries, user entries preserved
  • Preserve all other top-level keys in existing file
  • Write merged result with restrictive permissions (0o600)
  • Emit warning when writing to workspace scope (Windsurf mcp_config.json is global-only per docs)
// MCP config merge with security hardening
if (bundle.mcpConfig) {
  const mcpPath = path.join(outputRoot, "mcp_config.json")
  const backupPath = await backupFile(mcpPath)
  if (backupPath) {
    console.log(`Backed up existing mcp_config.json to ${backupPath}`)
  }

  let existingConfig: Record<string, unknown> = {}
  if (await pathExists(mcpPath)) {
    try {
      const parsed = await readJson<unknown>(mcpPath)
      if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
        existingConfig = parsed as Record<string, unknown>
      }
    } catch {
      console.warn("Warning: existing mcp_config.json could not be parsed and will be replaced.")
    }
  }

  const existingServers =
    existingConfig.mcpServers &&
    typeof existingConfig.mcpServers === "object" &&
    !Array.isArray(existingConfig.mcpServers)
      ? (existingConfig.mcpServers as Record<string, unknown>)
      : {}
  const merged = { ...existingConfig, mcpServers: { ...existingServers, ...bundle.mcpConfig.mcpServers } }
  await writeJsonSecure(mcpPath, merged)  // 0o600 permissions
}

Research Insights (Phase 3b)

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.

Implementation: Add a writeJsonSecure() helper or add a mode parameter to writeJson():

// src/utils/files.ts
export async function writeJsonSecure(filePath: string, data: unknown): Promise<void> {
  const content = JSON.stringify(data, null, 2)
  await ensureDir(path.dirname(filePath))
  await fs.writeFile(filePath, content + "\n", { encoding: "utf8", mode: 0o600 })
}

Security review (MEDIUM): Backup files inherit default permissions. Ensure backupFile() also sets 0o600 on the backup copy when the source may contain secrets.

Security review (MEDIUM): Workspace mcp_config.json could be committed to git. After writing to workspace scope, emit a warning:

Warning: .windsurf/mcp_config.json may contain secrets. Ensure it is in .gitignore.

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.

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(...).

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.

3c. Updated writer structure

export async function writeWindsurfBundle(outputRoot: string, bundle: WindsurfBundle): Promise<void> {
  await ensureDir(outputRoot)

  // Write agent workflows
  if (bundle.agentWorkflows.length > 0) {
    const agentDir = path.join(outputRoot, "workflows", "agents")
    await ensureDir(agentDir)
    for (const workflow of bundle.agentWorkflows) {
      validatePathSafe(workflow.name, "agent workflow")
      const content = formatFrontmatter({ description: workflow.description }, `# ${workflow.name}\n\n${workflow.body}`)
      await writeText(path.join(agentDir, `${workflow.name}.md`), content + "\n")
    }
  }

  // Write command workflows
  if (bundle.commandWorkflows.length > 0) {
    const cmdDir = path.join(outputRoot, "workflows", "commands")
    await ensureDir(cmdDir)
    for (const workflow of bundle.commandWorkflows) {
      validatePathSafe(workflow.name, "command workflow")
      const content = formatFrontmatter({ description: workflow.description }, `# ${workflow.name}\n\n${workflow.body}`)
      await writeText(path.join(cmdDir, `${workflow.name}.md`), content + "\n")
    }
  }

  // Copy skill directories
  if (bundle.skillDirs.length > 0) {
    const skillsDir = path.join(outputRoot, "skills")
    await ensureDir(skillsDir)
    for (const skill of bundle.skillDirs) {
      validatePathSafe(skill.name, "skill directory")
      const destDir = path.join(skillsDir, skill.name)
      const resolvedDest = path.resolve(destDir)
      if (!resolvedDest.startsWith(path.resolve(skillsDir))) {
        console.warn(`Warning: Skill name "${skill.name}" escapes skills/. Skipping.`)
        continue
      }
      await copyDir(skill.sourceDir, destDir)
    }
  }

  // Merge MCP config (see 3b above)
  if (bundle.mcpConfig) {
    // ... merge logic from 3b
  }
}

Phase 4: CLI Wiring

Files: src/commands/install.ts, src/commands/convert.ts

4a. Add --scope flag to both commands

scope: {
  type: "string",
  description: "Scope level: global | workspace (default varies by target)",
},
  • Add scope arg to install.ts
  • Add scope arg to convert.ts

4b. Validate scope with type guard

Use a proper type guard instead of unsafe as TargetScope cast:

function isTargetScope(value: string): value is TargetScope {
  return value === "global" || value === "workspace"
}

const scopeValue = args.scope ? String(args.scope) : undefined
if (scopeValue !== undefined) {
  if (!target.supportedScopes) {
    throw new Error(`Target "${targetName}" does not support the --scope flag.`)
  }
  if (!isTargetScope(scopeValue) || !target.supportedScopes.includes(scopeValue)) {
    throw new Error(`Target "${targetName}" does not support --scope ${scopeValue}. Supported: ${target.supportedScopes.join(", ")}`)
  }
}
const resolvedScope = scopeValue ?? target.defaultScope ?? "workspace"
  • Add isTargetScope type guard
  • Add scope validation in both commands (single block, not two separate checks)

Research Insights (Phase 4b)

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.

Simplicity review: The two-step validation (check supported, then check exists) can be a single block with the type guard approach above.

4c. Update output root resolution

Both commands now use the shared resolveTargetOutputRoot from Phase 0a.

  • Call shared function with scope: resolvedScope for primary target
  • Default scope: target.defaultScope ?? "workspace" (only used when target supports scopes)

4d. Handle --also targets

--scope applies only to the primary --to target. Extra --also targets use their own defaultScope.

  • Pass handler.defaultScope for --also targets (each uses its own default)
  • Update the --also loop in both commands to use target-specific scope resolution

Research Insights (Phase 4d)

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.

Phase 5: Tests

Files: tests/windsurf-converter.test.ts, tests/windsurf-writer.test.ts

5a. Update converter tests

  • Remove all AGENTS.md tests (lines 275-303: empty plugin, CLAUDE.md missing)
  • Remove all mcpSetupDoc tests (lines 305-366: stdio, HTTP/SSE, redaction, null)
  • Update fixturePlugin default — remove agentsMd and mcpSetupDoc references
  • Add mcpConfig tests:
    • stdio server produces correct JSON structure with command, args, env
    • HTTP/SSE server produces correct JSON structure with serverUrl, headers
    • mixed servers (stdio + HTTP) both included
    • env vars included (not redacted) — verify actual values present
    • hasPotentialSecrets() emits console.warn for sensitive keys
    • hasPotentialSecrets() does NOT warn when no sensitive keys
    • no servers produces null mcpConfig
    • empty bundle has null mcpConfig
    • server with no command and no URL is skipped with warning

5b. Update writer tests

  • Remove AGENTS.md tests (backup test, creation test, double-nesting AGENTS.md parent test)
  • Remove double-nesting guard test (guard removed)
  • Remove mcp-setup.md write test
  • Update emptyBundle fixture — remove agentsMd, mcpSetupDoc, add mcpConfig: null
  • Add mcp_config.json tests:
    • writes mcp_config.json to outputRoot
    • merges with existing mcp_config.json (preserves user servers)
    • backs up existing mcp_config.json before overwrite
    • handles corrupted existing mcp_config.json (warn and replace)
    • handles existing mcp_config.json with array (not object) at root
    • handles existing mcp_config.json with mcpServers: null
    • preserves non-mcpServers keys in existing file
    • server name collision: plugin entry wins
    • file permissions are 0o600 (not world-readable)
  • Update full bundle test — writer writes directly into outputRoot (no .windsurf/ nesting)

5c. Add scope resolution tests

Test the shared resolveTargetOutputRoot function:

  • Default scope for windsurf is "global" → resolves to ~/.codeium/windsurf/
  • Explicit --scope workspace → resolves to cwd/.windsurf/
  • --output overrides scope resolution (both global and workspace)
  • Invalid scope value for windsurf → error
  • --scope on non-scope target (e.g., opencode) → error
  • --also windsurf uses windsurf's default scope ("global")
  • isTargetScope type guard correctly identifies valid/invalid values

Phase 6: Documentation

Files: README.md, CHANGELOG.md

  • Update README.md Windsurf section to mention --scope flag and global default
  • Add CHANGELOG entry for v0.11.0 with breaking changes documented
  • Document migration path: --scope workspace for old behavior
  • Note that Windsurf mcp_config.json is global-only (workspace MCP config may not be discovered)

Acceptance Criteria

  • install compound-engineering --to windsurf writes to ~/.codeium/windsurf/ by default
  • install compound-engineering --to windsurf --scope workspace writes to cwd/.windsurf/
  • --output /custom/path overrides scope for both commands
  • --scope on non-supporting target produces clear error
  • mcp_config.json merges with existing file (backup created, user entries preserved)
  • mcp_config.json written with 0o600 permissions (not world-readable)
  • No AGENTS.md generated for either scope
  • Env var secrets included in mcp_config.json with console.warn listing affected servers
  • Both stdio and HTTP/SSE MCP servers included in mcp_config.json
  • All existing tests updated, all new tests pass
  • No regressions in other targets
  • resolveTargetOutputRoot extracted to shared utility (no duplication)

Dependencies & Risks

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.

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.

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.

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.

References & Research