From cf1245b8c5d3611b92cd0c682fea4e719af8e0c4 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 29 Mar 2026 07:27:09 +0000 Subject: [PATCH 1/2] feat: auto-detect installed harnesses from project artifacts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of defaulting to ["universal"], the configure and install commands now detect which harnesses are already present in the project by checking for characteristic top-level artifacts: - Universal: AGENTS.md - Claude Code: .claude/ - Cursor: .cursor/ - GitHub Copilot: .github/agents/ or .vscode/mcp.json - Windsurf: .windsurf/ or .windsurfrules - Cline: .clinerules or cline_mcp_settings.json - Roo Code: .roo/ or .roomodes or .roorules - Kiro: .kiro/ - OpenCode: opencode.json or .opencode/ Each HarnessWriter now implements a detect(projectRoot) method. detectHarnesses() in index.ts runs all detectors in parallel. In configure, detected harnesses are pre-selected as initial values (still overridable by saved lock file harnesses). In install, detected harnesses are used as the fallback when no --harness flag or lock file harnesses field is present, with "universal" as the last resort if nothing is detected. https://claude.ai/code/session_01GZ3o2r8bg3ry68sJvuh5pH feat: mark harness detection as verified or unverified Adds detectionVerified: boolean to HarnessWriter. Verified harnesses (universal, copilot, kiro, opencode) have had their detect() criteria confirmed against real-world artifacts. Unverified ones (claude-code, cursor, windsurf, cline, roo-code) use inferred heuristics and need feedback from users of those tools. The configure wizard appends "· auto-detect unverified" to the hint for unverified harnesses so users can see at a glance which detections are confirmed and which may need adjustment. https://claude.ai/code/session_01GZ3o2r8bg3ry68sJvuh5pH refactor: rename detectionVerified→verified, reorder harnesses, update hint - Rename detectionVerified → verified on HarnessWriter: the flag covers the whole harness (install + detect), not just detection - Wizard hint for unverified harnesses now reads "· unverified — config generation may be inaccurate" - allHarnessWriters reordered so verified harnesses (universal, copilot, kiro, opencode) appear first, unverified ones (claude-code, cursor, windsurf, cline, roo-code) appear after https://claude.ai/code/session_01GZ3o2r8bg3ry68sJvuh5pH style: format install.ts https://claude.ai/code/session_01GZ3o2r8bg3ry68sJvuh5pH fix: move pathExists above clack/core imports in util.ts fix: update HarnessWriterSchema to include verified and detect fix: update harness ID order in index.spec.ts --- packages/cli/src/commands/configure.ts | 16 ++++++----- packages/cli/src/commands/install.ts | 10 +++++-- packages/core/src/types.ts | 14 ++++++++-- packages/harnesses/src/index.spec.ts | 10 +++---- packages/harnesses/src/index.ts | 27 +++++++++++++++---- packages/harnesses/src/types.ts | 12 +++++++++ packages/harnesses/src/util.ts | 10 +++++++ packages/harnesses/src/writers/claude-code.ts | 7 ++++- packages/harnesses/src/writers/cline.ts | 10 ++++++- packages/harnesses/src/writers/copilot.ts | 10 ++++++- packages/harnesses/src/writers/cursor.ts | 6 ++++- packages/harnesses/src/writers/kiro.ts | 7 ++++- packages/harnesses/src/writers/opencode.ts | 10 ++++++- packages/harnesses/src/writers/roo-code.ts | 11 +++++++- packages/harnesses/src/writers/universal.ts | 6 ++++- packages/harnesses/src/writers/windsurf.ts | 10 ++++++- 16 files changed, 146 insertions(+), 30 deletions(-) diff --git a/packages/cli/src/commands/configure.ts b/packages/cli/src/commands/configure.ts index 46822e1..ba40743 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,22 @@ 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 savedHarnesses = lockFile.harnesses; + const initialHarnesses = savedHarnesses + ? savedHarnesses.filter((h) => harnessWriters.some((w) => w.id === h)) + : 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.ts b/packages/cli/src/commands/install.ts index 17fdf19..82b24dc 100644 --- a/packages/cli/src/commands/install.ts +++ b/packages/cli/src/commands/install.ts @@ -5,6 +5,7 @@ import { allHarnessWriters, getHarnessWriter, getHarnessIds, + detectHarnesses, installSkills, writeInlineSkills } from "@codemcp/ade-harnesses"; @@ -25,8 +26,13 @@ export async function runInstall( // 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"]; + // 3. auto-detected from project artifacts (falls back to universal if none found) + const ids = + harnessIds ?? + lockFile.harnesses ?? + (await detectHarnesses(projectRoot, harnessWriters).then((detected) => + detected.length > 0 ? detected : ["universal"] + )); const validIds = [...getHarnessIds(), ...harnessWriters.map((w) => w.id)]; const uniqueValidIds = [...new Set(validIds)]; diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 7f54a65..1ab0880 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -203,7 +203,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 +216,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..30f3281 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" ]); }); 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"), From 893d60fa79d3bc16988948ca3ca82155c00dbc40 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 29 Mar 2026 08:51:30 +0000 Subject: [PATCH 2/2] chore: remove harnesses from install, drop dead LockFile field harnesses was never written to the lock file (setup omits it, configure is ephemeral), so lockFile.harnesses was always undefined. Remove the field from LockFile and UserConfig types, simplify configure to always use detectHarnesses(), and simplify install to resolve via --harness flag or auto-detection only. Tests updated accordingly: lock-file-harnesses test replaced with auto-detection test, detectHarnesses mock added to both suites. https://claude.ai/code/session_01GZ3o2r8bg3ry68sJvuh5pH refactor: remove harness handling from ade install ade install now only handles skills and knowledge sources. Harness installation belongs exclusively to ade configure. - runInstall() signature simplified (no harnessIds or harnessWriters params) - --harness flag removed from CLI - detectHarnesses, getHarnessIds, getHarnessWriter removed from install - index.ts install branch simplified to a single line - install.spec.ts stripped of all harness-related tests and mocks https://claude.ai/code/session_01GZ3o2r8bg3ry68sJvuh5pH fix: update specs for removed harness args and new HarnessWriter fields fix: add verified/detect to mock HarnessWriter in index.spec.ts --- packages/cli/src/commands/configure.spec.ts | 11 +- packages/cli/src/commands/configure.ts | 5 +- .../src/commands/install.integration.spec.ts | 46 +----- packages/cli/src/commands/install.spec.ts | 141 +++--------------- packages/cli/src/commands/install.ts | 45 +----- .../knowledge-docset.integration.spec.ts | 2 +- packages/cli/src/index.ts | 21 +-- packages/core/src/extensions.spec.ts | 2 + packages/core/src/types.ts | 2 - packages/harnesses/src/index.spec.ts | 4 + 10 files changed, 39 insertions(+), 240 deletions(-) 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 ba40743..40e0f6d 100644 --- a/packages/cli/src/commands/configure.ts +++ b/packages/cli/src/commands/configure.ts @@ -80,10 +80,7 @@ export async function runConfigure( : `${w.description} · unverified — config generation may be inaccurate` })); - const savedHarnesses = lockFile.harnesses; - const initialHarnesses = savedHarnesses - ? savedHarnesses.filter((h) => harnessWriters.some((w) => w.id === h)) - : await detectHarnesses(projectRoot, harnessWriters); + const initialHarnesses = await detectHarnesses(projectRoot, harnessWriters); const selectedHarnesses = await clack.multiselect({ message: 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 82b24dc..ef9a53e 100644 --- a/packages/cli/src/commands/install.ts +++ b/packages/cli/src/commands/install.ts @@ -1,21 +1,9 @@ import * as clack from "@clack/prompts"; import { readLockFile } from "@codemcp/ade-core"; -import { - type HarnessWriter, - allHarnessWriters, - getHarnessWriter, - getHarnessIds, - detectHarnesses, - 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); @@ -23,37 +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. auto-detected from project artifacts (falls back to universal if none found) - const ids = - harnessIds ?? - lockFile.harnesses ?? - (await detectHarnesses(projectRoot, harnessWriters).then((detected) => - detected.length > 0 ? detected : ["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 1ab0880..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; } diff --git a/packages/harnesses/src/index.spec.ts b/packages/harnesses/src/index.spec.ts index 30f3281..4752979 100644 --- a/packages/harnesses/src/index.spec.ts +++ b/packages/harnesses/src/index.spec.ts @@ -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 () => {} } ]