diff --git a/packages/cli/src/commands/configure.spec.ts b/packages/cli/src/commands/configure.spec.ts index dfe34c8..dad956a 100644 --- a/packages/cli/src/commands/configure.spec.ts +++ b/packages/cli/src/commands/configure.spec.ts @@ -40,6 +40,7 @@ vi.mock("@codemcp/ade-harnesses", () => ({ } return undefined; }), + detectHarnesses: vi.fn().mockResolvedValue([]), installSkills: mockInstallSkills, writeInlineSkills: mockWriteInlineSkills })); @@ -69,7 +70,6 @@ const baseLockFile: LockFile = { version: 1, generated_at: "2024-01-01T00:00:00.000Z", choices: { process: "codemcp-workflows" }, - harnesses: ["universal"], logical_config: { mcp_servers: [], instructions: ["do stuff"], @@ -181,11 +181,10 @@ describe("runConfigure", () => { expect(mockInstall).not.toHaveBeenCalled(); }); - it("uses lock file harnesses as initial selection for harness prompt", async () => { - vi.mocked(readLockFile).mockResolvedValueOnce({ - ...baseLockFile, - harnesses: ["cursor"] - }); + it("uses auto-detected harnesses as initial selection for harness prompt", async () => { + const { detectHarnesses } = await import("@codemcp/ade-harnesses"); + vi.mocked(detectHarnesses).mockResolvedValueOnce(["cursor"]); + vi.mocked(readLockFile).mockResolvedValueOnce(baseLockFile); vi.mocked(clack.select).mockResolvedValueOnce("sensible-defaults"); vi.mocked(clack.multiselect).mockResolvedValueOnce(["cursor"]); diff --git a/packages/cli/src/commands/configure.ts b/packages/cli/src/commands/configure.ts index 46822e1..40e0f6d 100644 --- a/packages/cli/src/commands/configure.ts +++ b/packages/cli/src/commands/configure.ts @@ -10,6 +10,7 @@ import { type HarnessWriter, allHarnessWriters, getHarnessWriter, + detectHarnesses, installSkills, writeInlineSkills } from "@codemcp/ade-harnesses"; @@ -74,21 +75,19 @@ export async function runConfigure( const harnessOptions = harnessWriters.map((w) => ({ value: w.id, label: w.label, - hint: w.description + hint: w.verified + ? w.description + : `${w.description} · unverified — config generation may be inaccurate` })); - const existingHarnesses = lockFile.harnesses ?? ["universal"]; - const validInitialHarnesses = existingHarnesses.filter((h) => - harnessWriters.some((w) => w.id === h) - ); + const initialHarnesses = await detectHarnesses(projectRoot, harnessWriters); const selectedHarnesses = await clack.multiselect({ message: "Which coding agents should receive this configuration?\n" + "ADE generates config files for each agent you select.\n", options: harnessOptions, - initialValues: - validInitialHarnesses.length > 0 ? validInitialHarnesses : ["universal"], + initialValues: initialHarnesses, required: false }); diff --git a/packages/cli/src/commands/install.integration.spec.ts b/packages/cli/src/commands/install.integration.spec.ts index c8c3579..f55b8e2 100644 --- a/packages/cli/src/commands/install.integration.spec.ts +++ b/packages/cli/src/commands/install.integration.spec.ts @@ -40,37 +40,21 @@ describe("install integration (real temp dir)", () => { await rm(dir, { recursive: true, force: true }); }); - it("applies lock file to regenerate agent files without re-resolving", async () => { + it("completes without error after setup", async () => { const catalog = getDefaultCatalog(); - // Step 1: Run setup to create config.yaml + config.lock.yaml vi.mocked(clack.select) .mockResolvedValueOnce("codemcp-workflows") // process .mockResolvedValueOnce("other"); // architecture vi.mocked(clack.multiselect).mockResolvedValueOnce([]); // practices: none await runSetup(dir, catalog); - // Step 2: Run install — writes agent files from lock file - await runInstall(dir, ["claude-code"]); - - // Agent files should be written by install - const agentMd = await readFile( - join(dir, ".claude", "agents", "ade.md"), - "utf-8" - ); - expect(agentMd).toContain("Call whats_next()"); - - const mcpJson = JSON.parse(await readFile(join(dir, ".mcp.json"), "utf-8")); - expect(mcpJson.mcpServers["workflows"]).toMatchObject({ - command: "npx", - args: ["@codemcp/workflows-server@latest"] - }); + await runInstall(dir); }); it("does not modify the lock file", async () => { const catalog = getDefaultCatalog(); - // Setup first vi.mocked(clack.select) .mockResolvedValueOnce("codemcp-workflows") // process .mockResolvedValueOnce("other"); // architecture @@ -82,37 +66,15 @@ describe("install integration (real temp dir)", () => { "utf-8" ); - // Re-install - await runInstall(dir, ["claude-code"]); + await runInstall(dir); const lockRawAfter = await readFile(join(dir, "config.lock.yaml"), "utf-8"); - // Lock file should be byte-identical (install doesn't rewrite it) expect(lockRawAfter).toBe(lockRawBefore); }); it("fails when no config.lock.yaml exists", async () => { - await expect(runInstall(dir, ["claude-code"])).rejects.toThrow( + await expect(runInstall(dir)).rejects.toThrow( /config\.lock\.yaml not found/i ); }); - - it("works with native-agents-md option", async () => { - const catalog = getDefaultCatalog(); - - // Setup with native-agents-md - vi.mocked(clack.select) - .mockResolvedValueOnce("native-agents-md") // process - .mockResolvedValueOnce("other"); // architecture - vi.mocked(clack.multiselect).mockResolvedValueOnce([]); // practices: none - await runSetup(dir, catalog); - - // Run install - await runInstall(dir, ["claude-code"]); - - const agentMd = await readFile( - join(dir, ".claude", "agents", "ade.md"), - "utf-8" - ); - expect(agentMd).toContain("AGENTS.md"); - }); }); diff --git a/packages/cli/src/commands/install.spec.ts b/packages/cli/src/commands/install.spec.ts index f281003..8712d58 100644 --- a/packages/cli/src/commands/install.spec.ts +++ b/packages/cli/src/commands/install.spec.ts @@ -6,6 +6,8 @@ import type { LogicalConfig } from "@codemcp/ade-core"; vi.mock("@clack/prompts", () => ({ intro: vi.fn(), outro: vi.fn(), + confirm: vi.fn().mockResolvedValue(true), + cancel: vi.fn(), log: { info: vi.fn(), warn: vi.fn(), error: vi.fn() } })); @@ -27,131 +29,42 @@ vi.mock("@codemcp/ade-core", async (importOriginal) => { }; }); -const mockInstall = vi.hoisted(() => vi.fn().mockResolvedValue(undefined)); - vi.mock("@codemcp/ade-harnesses", () => ({ - allHarnessWriters: [ - { - id: "universal", - label: "Universal", - description: "Universal", - install: mockInstall - }, - { - id: "claude-code", - label: "Claude Code", - description: "Claude Code", - install: mockInstall - }, - { - id: "cursor", - label: "Cursor", - description: "Cursor", - install: mockInstall - } - ], - getHarnessWriter: vi.fn().mockImplementation((id: string) => { - if (id === "universal" || id === "claude-code" || id === "cursor") { - return { id, install: mockInstall }; - } - return undefined; - }), - getHarnessIds: vi - .fn() - .mockReturnValue([ - "universal", - "claude-code", - "cursor", - "copilot", - "windsurf", - "cline", - "roo-code", - "kiro", - "opencode" - ]), installSkills: vi.fn().mockResolvedValue(undefined), writeInlineSkills: vi.fn().mockResolvedValue([]) })); +vi.mock("../knowledge-installer.js", () => ({ + installKnowledge: vi.fn().mockResolvedValue(undefined) +})); + import * as clack from "@clack/prompts"; import { readLockFile } from "@codemcp/ade-core"; import { runInstall } from "./install.js"; // ── Tests ──────────────────────────────────────────────────────────────────── +const baseLockFile = { + version: 1 as const, + generated_at: "2024-01-01T00:00:00.000Z", + choices: { process: "codemcp-workflows" }, + logical_config: mockLogical +}; + describe("runInstall", () => { - beforeEach(async () => { + beforeEach(() => { vi.clearAllMocks(); - // Re-set the default implementation after clearAllMocks - const { getHarnessWriter } = await import("@codemcp/ade-harnesses"); - vi.mocked(getHarnessWriter).mockImplementation((id: string) => { - if (id === "universal" || id === "claude-code" || id === "cursor") { - return { - id, - label: id, - description: "test", - install: mockInstall - }; - } - return undefined; - }); + vi.mocked(clack.confirm).mockResolvedValue(true); }); - it("reads config.lock.yaml and applies logical config", async () => { - vi.mocked(readLockFile).mockResolvedValueOnce({ - version: 1, - generated_at: "2024-01-01T00:00:00.000Z", - choices: { process: "codemcp-workflows" }, - logical_config: mockLogical - }); + it("reads config.lock.yaml", async () => { + vi.mocked(readLockFile).mockResolvedValueOnce(baseLockFile); await runInstall("/tmp/project"); expect(readLockFile).toHaveBeenCalledWith("/tmp/project"); }); - it("defaults to universal harness when none specified", async () => { - vi.mocked(readLockFile).mockResolvedValueOnce({ - version: 1, - generated_at: "2024-01-01T00:00:00.000Z", - choices: { process: "codemcp-workflows" }, - logical_config: mockLogical - }); - - await runInstall("/tmp/project"); - - expect(mockInstall).toHaveBeenCalledWith(mockLogical, "/tmp/project"); - }); - - it("uses harnesses from lock file when present", async () => { - vi.mocked(readLockFile).mockResolvedValueOnce({ - version: 1, - generated_at: "2024-01-01T00:00:00.000Z", - choices: { process: "codemcp-workflows" }, - harnesses: ["claude-code", "cursor"], - logical_config: mockLogical - }); - - await runInstall("/tmp/project"); - - expect(mockInstall).toHaveBeenCalledTimes(2); - }); - - it("uses explicit harness ids when provided", async () => { - vi.mocked(readLockFile).mockResolvedValueOnce({ - version: 1, - generated_at: "2024-01-01T00:00:00.000Z", - choices: { process: "codemcp-workflows" }, - harnesses: ["claude-code"], - logical_config: mockLogical - }); - - await runInstall("/tmp/project", ["cursor"]); - - // Explicit takes priority over lock file - expect(mockInstall).toHaveBeenCalledTimes(1); - }); - it("throws when config.lock.yaml is missing", async () => { vi.mocked(readLockFile).mockResolvedValueOnce(null); @@ -160,26 +73,8 @@ describe("runInstall", () => { ); }); - it("throws when harness id is unknown", async () => { - vi.mocked(readLockFile).mockResolvedValueOnce({ - version: 1, - generated_at: "2024-01-01T00:00:00.000Z", - choices: { process: "codemcp-workflows" }, - logical_config: mockLogical - }); - - await expect(runInstall("/tmp/project", ["unknown-agent"])).rejects.toThrow( - /unknown harness/i - ); - }); - it("shows intro and outro messages", async () => { - vi.mocked(readLockFile).mockResolvedValueOnce({ - version: 1, - generated_at: "2024-01-01T00:00:00.000Z", - choices: { process: "codemcp-workflows" }, - logical_config: mockLogical - }); + vi.mocked(readLockFile).mockResolvedValueOnce(baseLockFile); await runInstall("/tmp/project"); diff --git a/packages/cli/src/commands/install.ts b/packages/cli/src/commands/install.ts index 17fdf19..ef9a53e 100644 --- a/packages/cli/src/commands/install.ts +++ b/packages/cli/src/commands/install.ts @@ -1,20 +1,9 @@ import * as clack from "@clack/prompts"; import { readLockFile } from "@codemcp/ade-core"; -import { - type HarnessWriter, - allHarnessWriters, - getHarnessWriter, - getHarnessIds, - installSkills, - writeInlineSkills -} from "@codemcp/ade-harnesses"; +import { installSkills, writeInlineSkills } from "@codemcp/ade-harnesses"; import { installKnowledge } from "../knowledge-installer.js"; -export async function runInstall( - projectRoot: string, - harnessIds?: string[], - harnessWriters: HarnessWriter[] = allHarnessWriters -): Promise { +export async function runInstall(projectRoot: string): Promise { clack.intro("ade install"); const lockFile = await readLockFile(projectRoot); @@ -22,32 +11,8 @@ export async function runInstall( throw new Error("config.lock.yaml not found. Run `ade setup` first."); } - // Determine which harnesses to install for: - // 1. --harness flag (comma-separated) - // 2. harnesses saved in the lock file - // 3. default: universal - const ids = harnessIds ?? lockFile.harnesses ?? ["universal"]; - - const validIds = [...getHarnessIds(), ...harnessWriters.map((w) => w.id)]; - const uniqueValidIds = [...new Set(validIds)]; - for (const id of ids) { - if (!uniqueValidIds.includes(id)) { - throw new Error( - `Unknown harness "${id}". Available: ${uniqueValidIds.join(", ")}` - ); - } - } - const logicalConfig = lockFile.logical_config; - for (const id of ids) { - const writer = - harnessWriters.find((w) => w.id === id) ?? getHarnessWriter(id); - if (writer) { - await writer.install(logicalConfig, projectRoot); - } - } - const modifiedSkills = await writeInlineSkills(logicalConfig, projectRoot); if (modifiedSkills.length > 0) { clack.log.warn( diff --git a/packages/cli/src/commands/knowledge-docset.integration.spec.ts b/packages/cli/src/commands/knowledge-docset.integration.spec.ts index 276ede3..47e1391 100644 --- a/packages/cli/src/commands/knowledge-docset.integration.spec.ts +++ b/packages/cli/src/commands/knowledge-docset.integration.spec.ts @@ -133,7 +133,7 @@ describe("knowledge docset regression tests", () => { await rm(join(dir, ".knowledge"), { recursive: true, force: true }); // Now run install — should also write .knowledge/config.yaml - await runInstall(dir, ["claude-code"]); + await runInstall(dir); // All 4 tanstack docsets are configured via the docset writer expect(createDocset).toHaveBeenCalledTimes(4); diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 8f095e8..a6e671d 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -3,7 +3,7 @@ import { runSetup } from "./commands/setup.js"; import { runInstall } from "./commands/install.js"; import { runConfigure } from "./commands/configure.js"; import { getDefaultCatalog, mergeExtensions } from "@codemcp/ade-core"; -import { getHarnessIds, buildHarnessWriters } from "@codemcp/ade-harnesses"; +import { buildHarnessWriters } from "@codemcp/ade-harnesses"; import { loadExtensions } from "./extensions.js"; const args = process.argv.slice(2); @@ -17,20 +17,7 @@ if (command === "setup") { await runSetup(projectRoot, catalog, harnessWriters); } else if (command === "install") { const projectRoot = args[1] ?? process.cwd(); - const extensions = await loadExtensions(projectRoot); - const harnessWriters = buildHarnessWriters(extensions); - - let harnessIds: string[] | undefined; - - // Support --harness flag (comma-separated) - if (args.includes("--harness")) { - const val = args[args.indexOf("--harness") + 1]; - if (val) { - harnessIds = val.split(",").map((s) => s.trim()); - } - } - - await runInstall(projectRoot, harnessIds, harnessWriters); + await runInstall(projectRoot); } else if (command === "configure") { const projectRoot = args[1] ?? process.cwd(); const extensions = await loadExtensions(projectRoot); @@ -39,7 +26,6 @@ if (command === "setup") { } else if (command === "--version" || command === "-v") { console.log(version); } else { - const allIds = getHarnessIds(); console.log(`ade v${version} — Agentic Development Environment`); console.log(); console.log( @@ -63,9 +49,6 @@ if (command === "setup") { ); console.log(); console.log("Options:"); - console.log( - ` --harness Comma-separated harnesses (${allIds.join(", ")})` - ); console.log(" -v, --version Show version"); process.exitCode = command ? 1 : 0; } diff --git a/packages/core/src/extensions.spec.ts b/packages/core/src/extensions.spec.ts index 2838559..5c08e6d 100644 --- a/packages/core/src/extensions.spec.ts +++ b/packages/core/src/extensions.spec.ts @@ -52,6 +52,8 @@ describe("AdeExtensionsSchema", () => { id: "my-harness", label: "My Harness", description: "Custom harness", + verified: false, + detect: async () => false, install: async () => {} } ] diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 7f54a65..184323c 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -134,7 +134,6 @@ export interface ResolvedFacet { export interface UserConfig { choices: Record; - harnesses?: string[]; custom?: { mcp_servers?: McpServerEntry[]; instructions?: string[]; @@ -145,7 +144,6 @@ export interface LockFile { version: 1; generated_at: string; choices: Record; - harnesses?: string[]; logical_config: LogicalConfig; } @@ -203,7 +201,12 @@ const FacetSchema = z.custom( ); const HarnessWriterSchema = z.custom< - AgentWriterDef & { label: string; description: string } + AgentWriterDef & { + label: string; + description: string; + verified: boolean; + detect: (projectRoot: string) => Promise; + } >( (val) => typeof val === "object" && @@ -211,8 +214,13 @@ const HarnessWriterSchema = z.custom< typeof (val as Record).id === "string" && typeof (val as Record).label === "string" && typeof (val as Record).description === "string" && + typeof (val as Record).verified === "boolean" && + typeof (val as Record).detect === "function" && typeof (val as Record).install === "function", - { message: "HarnessWriter must have id, label, description and install()" } + { + message: + "HarnessWriter must have id, label, description, verified, detect() and install()" + } ); const ProvisionWriterDefSchema = z.custom( diff --git a/packages/harnesses/src/index.spec.ts b/packages/harnesses/src/index.spec.ts index 7ddcdd2..4752979 100644 --- a/packages/harnesses/src/index.spec.ts +++ b/packages/harnesses/src/index.spec.ts @@ -27,18 +27,18 @@ describe("harness registry", () => { expect(getHarnessWriter("nonexistent")).toBeUndefined(); }); - it("returns all harness ids", () => { + it("returns all harness ids, verified ones first", () => { const ids = getHarnessIds(); expect(ids).toEqual([ "universal", + "copilot", + "kiro", + "opencode", "claude-code", "cursor", - "copilot", "windsurf", "cline", - "roo-code", - "kiro", - "opencode" + "roo-code" ]); }); @@ -64,6 +64,8 @@ describe("buildHarnessWriters", () => { id: "sap-copilot", label: "SAP Copilot", description: "SAP internal Copilot harness", + verified: false, + detect: async () => false, install: async () => {} }; @@ -83,6 +85,8 @@ describe("buildHarnessWriters", () => { id: "ephemeral", label: "Ephemeral", description: "Should not persist", + verified: false, + detect: async () => false, install: async () => {} } ] diff --git a/packages/harnesses/src/index.ts b/packages/harnesses/src/index.ts index 745628e..3d062ed 100644 --- a/packages/harnesses/src/index.ts +++ b/packages/harnesses/src/index.ts @@ -23,17 +23,19 @@ import { rooCodeWriter } from "./writers/roo-code.js"; import { kiroWriter } from "./writers/kiro.js"; import { opencodeWriter } from "./writers/opencode.js"; -/** All built-in harness writers, ordered for wizard display. */ +/** All built-in harness writers, verified ones first, then unverified. */ export const allHarnessWriters: HarnessWriter[] = [ + // verified universalWriter, + copilotWriter, + kiroWriter, + opencodeWriter, + // unverified — config generation may be inaccurate; feedback welcome claudeCodeWriter, cursorWriter, - copilotWriter, windsurfWriter, clineWriter, - rooCodeWriter, - kiroWriter, - opencodeWriter + rooCodeWriter ]; /** Look up a harness writer by id. */ @@ -55,3 +57,18 @@ export function buildHarnessWriters(extensions: { }): HarnessWriter[] { return [...allHarnessWriters, ...(extensions.harnessWriters ?? [])]; } + +/** + * Auto-detects which harnesses are installed in the project by checking each + * writer's characteristic top-level artifacts. Returns an array of detected + * harness IDs, or an empty array if none are found. + */ +export async function detectHarnesses( + projectRoot: string, + writers: HarnessWriter[] = allHarnessWriters +): Promise { + const results = await Promise.all( + writers.map(async (w) => ((await w.detect(projectRoot)) ? w.id : null)) + ); + return results.filter((id): id is string => id !== null); +} diff --git a/packages/harnesses/src/types.ts b/packages/harnesses/src/types.ts index a549a4f..054d300 100644 --- a/packages/harnesses/src/types.ts +++ b/packages/harnesses/src/types.ts @@ -9,4 +9,16 @@ export interface HarnessWriter extends AgentWriterDef { label: string; /** Short description shown as hint in the wizard */ description: string; + /** + * Whether this harness writer has been verified against the actual tool. + * Unverified harnesses were contributed without hands-on testing — both the + * generated config and auto-detection heuristics may be inaccurate. Feedback + * from users of those tools is welcome. + */ + verified: boolean; + /** + * Returns true if this harness appears to be installed in the project by + * checking for its characteristic top-level artifacts (config files, dirs). + */ + detect(projectRoot: string): Promise; } diff --git a/packages/harnesses/src/util.ts b/packages/harnesses/src/util.ts index 4ac1e8d..690c4aa 100644 --- a/packages/harnesses/src/util.ts +++ b/packages/harnesses/src/util.ts @@ -3,6 +3,16 @@ import { dirname, join } from "node:path"; import * as clack from "@clack/prompts"; import type { GitHook, LogicalConfig, McpServerEntry } from "@codemcp/ade-core"; +/** Returns true if the given path exists (file or directory). */ +export async function pathExists(path: string): Promise { + try { + await access(path); + return true; + } catch { + return false; + } +} + // --------------------------------------------------------------------------- // JSON helpers // --------------------------------------------------------------------------- diff --git a/packages/harnesses/src/writers/claude-code.ts b/packages/harnesses/src/writers/claude-code.ts index 4d96bb4..0747d13 100644 --- a/packages/harnesses/src/writers/claude-code.ts +++ b/packages/harnesses/src/writers/claude-code.ts @@ -6,7 +6,8 @@ import { writeJson, writeMcpServers, writeAgentMd, - writeGitHooks + writeGitHooks, + pathExists } from "../util.js"; import { getAutonomyProfile } from "../permission-policy.js"; @@ -15,6 +16,10 @@ export const claudeCodeWriter: HarnessWriter = { label: "Claude Code", description: "Anthropic's CLI agent — .claude/agents/ade.md + .mcp.json + .claude/settings.json", + verified: false, + async detect(projectRoot: string) { + return pathExists(join(projectRoot, ".claude")); + }, async install(config: LogicalConfig, projectRoot: string) { await writeAgentMd(config, { path: join(projectRoot, ".claude", "agents", "ade.md"), diff --git a/packages/harnesses/src/writers/cline.ts b/packages/harnesses/src/writers/cline.ts index bbb9965..d7ddee9 100644 --- a/packages/harnesses/src/writers/cline.ts +++ b/packages/harnesses/src/writers/cline.ts @@ -5,13 +5,21 @@ import { writeMcpServers, alwaysAllowEntry, writeRulesFile, - writeGitHooks + writeGitHooks, + pathExists } from "../util.js"; export const clineWriter: HarnessWriter = { id: "cline", label: "Cline", description: "VS Code AI agent — cline_mcp_settings.json + .clinerules", + verified: false, + async detect(projectRoot: string) { + return ( + (await pathExists(join(projectRoot, ".clinerules"))) || + (await pathExists(join(projectRoot, "cline_mcp_settings.json"))) + ); + }, async install(config: LogicalConfig, projectRoot: string) { await writeMcpServers(config.mcp_servers, { path: join(projectRoot, "cline_mcp_settings.json"), diff --git a/packages/harnesses/src/writers/copilot.ts b/packages/harnesses/src/writers/copilot.ts index 1c43883..2bf7e97 100644 --- a/packages/harnesses/src/writers/copilot.ts +++ b/packages/harnesses/src/writers/copilot.ts @@ -10,7 +10,8 @@ import { stdioEntry, writeAgentMd, writeGitHooks, - formatYamlKey + formatYamlKey, + pathExists } from "../util.js"; import { getAutonomyProfile } from "../permission-policy.js"; @@ -18,6 +19,13 @@ export const copilotWriter: HarnessWriter = { id: "copilot", label: "GitHub Copilot", description: "VS Code + CLI — .vscode/mcp.json + .github/agents/ade.agent.md", + verified: true, + async detect(projectRoot: string) { + return ( + (await pathExists(join(projectRoot, ".github", "agents"))) || + (await pathExists(join(projectRoot, ".vscode", "mcp.json"))) + ); + }, async install(config: LogicalConfig, projectRoot: string) { await writeMcpServers(config.mcp_servers, { path: join(projectRoot, ".vscode", "mcp.json"), diff --git a/packages/harnesses/src/writers/cursor.ts b/packages/harnesses/src/writers/cursor.ts index 372fce0..a13b751 100644 --- a/packages/harnesses/src/writers/cursor.ts +++ b/packages/harnesses/src/writers/cursor.ts @@ -2,7 +2,7 @@ import { mkdir, writeFile } from "node:fs/promises"; import { join } from "node:path"; import type { AutonomyProfile, LogicalConfig } from "@codemcp/ade-core"; import type { HarnessWriter } from "../types.js"; -import { writeMcpServers, writeGitHooks } from "../util.js"; +import { writeMcpServers, writeGitHooks, pathExists } from "../util.js"; import { getAutonomyProfile, hasPermissionPolicy @@ -12,6 +12,10 @@ export const cursorWriter: HarnessWriter = { id: "cursor", label: "Cursor", description: "AI code editor — .cursor/mcp.json + .cursor/rules/", + verified: false, + async detect(projectRoot: string) { + return pathExists(join(projectRoot, ".cursor")); + }, async install(config: LogicalConfig, projectRoot: string) { await writeMcpServers(config.mcp_servers, { path: join(projectRoot, ".cursor", "mcp.json") diff --git a/packages/harnesses/src/writers/kiro.ts b/packages/harnesses/src/writers/kiro.ts index 55000f6..5483855 100644 --- a/packages/harnesses/src/writers/kiro.ts +++ b/packages/harnesses/src/writers/kiro.ts @@ -9,7 +9,8 @@ import { standardEntry, writeGitHooks, writeJson, - writeMcpServers + writeMcpServers, + pathExists } from "../util.js"; import { getAutonomyProfile } from "../permission-policy.js"; @@ -17,6 +18,10 @@ export const kiroWriter: HarnessWriter = { id: "kiro", label: "Kiro", description: "AWS AI IDE — .kiro/agents/ade.json + .kiro/settings/mcp.json", + verified: true, + async detect(projectRoot: string) { + return pathExists(join(projectRoot, ".kiro")); + }, async install(config: LogicalConfig, projectRoot: string) { await writeMcpServers(config.mcp_servers, { path: join(projectRoot, ".kiro", "settings", "mcp.json"), diff --git a/packages/harnesses/src/writers/opencode.ts b/packages/harnesses/src/writers/opencode.ts index 3b0e9b6..bf843c2 100644 --- a/packages/harnesses/src/writers/opencode.ts +++ b/packages/harnesses/src/writers/opencode.ts @@ -9,7 +9,8 @@ import { writeAgentMd, writeGitHooks, writeMcpServers, - formatYamlKey + formatYamlKey, + pathExists } from "../util.js"; import { getAutonomyProfile } from "../permission-policy.js"; @@ -178,6 +179,13 @@ export const opencodeWriter: HarnessWriter = { id: "opencode", label: "OpenCode", description: "Terminal AI agent — opencode.json + .opencode/agents/", + verified: true, + async detect(projectRoot: string) { + return ( + (await pathExists(join(projectRoot, "opencode.json"))) || + (await pathExists(join(projectRoot, ".opencode"))) + ); + }, async install(config: LogicalConfig, projectRoot: string) { await writeMcpServers(config.mcp_servers, { path: join(projectRoot, "opencode.json"), diff --git a/packages/harnesses/src/writers/roo-code.ts b/packages/harnesses/src/writers/roo-code.ts index 12f4428..ea7e47b 100644 --- a/packages/harnesses/src/writers/roo-code.ts +++ b/packages/harnesses/src/writers/roo-code.ts @@ -7,7 +7,8 @@ import { alwaysAllowEntry, writeRulesFile, writeGitHooks, - writeJson + writeJson, + pathExists } from "../util.js"; import { getAutonomyProfile, @@ -18,6 +19,14 @@ export const rooCodeWriter: HarnessWriter = { id: "roo-code", label: "Roo Code", description: "AI coding agent — .roo/mcp.json + .roomodes + .roorules", + verified: false, + async detect(projectRoot: string) { + return ( + (await pathExists(join(projectRoot, ".roo"))) || + (await pathExists(join(projectRoot, ".roomodes"))) || + (await pathExists(join(projectRoot, ".roorules"))) + ); + }, async install(config: LogicalConfig, projectRoot: string) { await writeMcpServers(config.mcp_servers, { path: join(projectRoot, ".roo", "mcp.json"), diff --git a/packages/harnesses/src/writers/universal.ts b/packages/harnesses/src/writers/universal.ts index 1631ede..316fdeb 100644 --- a/packages/harnesses/src/writers/universal.ts +++ b/packages/harnesses/src/writers/universal.ts @@ -2,7 +2,7 @@ import { join } from "node:path"; import { writeFile } from "node:fs/promises"; import type { AutonomyProfile, LogicalConfig } from "@codemcp/ade-core"; import type { HarnessWriter } from "../types.js"; -import { writeMcpServers, writeGitHooks } from "../util.js"; +import { writeMcpServers, writeGitHooks, pathExists } from "../util.js"; import { getAutonomyProfile } from "../permission-policy.js"; function renderAutonomyGuidance(config: LogicalConfig): string | undefined { @@ -70,6 +70,10 @@ export const universalWriter: HarnessWriter = { label: "Universal (AGENTS.md + .mcp.json)", description: "Cross-tool standard — AGENTS.md + .mcp.json (portable instructions and MCP registration, not enforceable permissions)", + verified: true, + async detect(projectRoot: string) { + return pathExists(join(projectRoot, "AGENTS.md")); + }, async install(config: LogicalConfig, projectRoot: string) { const autonomyGuidance = renderAutonomyGuidance(config); const instructionSections = [...config.instructions]; diff --git a/packages/harnesses/src/writers/windsurf.ts b/packages/harnesses/src/writers/windsurf.ts index f36ddcd..350d173 100644 --- a/packages/harnesses/src/writers/windsurf.ts +++ b/packages/harnesses/src/writers/windsurf.ts @@ -5,7 +5,8 @@ import { writeMcpServers, alwaysAllowEntry, writeRulesFile, - writeGitHooks + writeGitHooks, + pathExists } from "../util.js"; import { getAutonomyProfile, @@ -16,6 +17,13 @@ export const windsurfWriter: HarnessWriter = { id: "windsurf", label: "Windsurf", description: "Codeium's AI IDE — .windsurf/mcp.json + .windsurfrules", + verified: false, + async detect(projectRoot: string) { + return ( + (await pathExists(join(projectRoot, ".windsurf"))) || + (await pathExists(join(projectRoot, ".windsurfrules"))) + ); + }, async install(config: LogicalConfig, projectRoot: string) { await writeMcpServers(config.mcp_servers, { path: join(projectRoot, ".windsurf", "mcp.json"),