Skip to content

Commit 893d60f

Browse files
committed
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
1 parent cf1245b commit 893d60f

10 files changed

Lines changed: 39 additions & 240 deletions

File tree

packages/cli/src/commands/configure.spec.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ vi.mock("@codemcp/ade-harnesses", () => ({
4040
}
4141
return undefined;
4242
}),
43+
detectHarnesses: vi.fn().mockResolvedValue([]),
4344
installSkills: mockInstallSkills,
4445
writeInlineSkills: mockWriteInlineSkills
4546
}));
@@ -69,7 +70,6 @@ const baseLockFile: LockFile = {
6970
version: 1,
7071
generated_at: "2024-01-01T00:00:00.000Z",
7172
choices: { process: "codemcp-workflows" },
72-
harnesses: ["universal"],
7373
logical_config: {
7474
mcp_servers: [],
7575
instructions: ["do stuff"],
@@ -181,11 +181,10 @@ describe("runConfigure", () => {
181181
expect(mockInstall).not.toHaveBeenCalled();
182182
});
183183

184-
it("uses lock file harnesses as initial selection for harness prompt", async () => {
185-
vi.mocked(readLockFile).mockResolvedValueOnce({
186-
...baseLockFile,
187-
harnesses: ["cursor"]
188-
});
184+
it("uses auto-detected harnesses as initial selection for harness prompt", async () => {
185+
const { detectHarnesses } = await import("@codemcp/ade-harnesses");
186+
vi.mocked(detectHarnesses).mockResolvedValueOnce(["cursor"]);
187+
vi.mocked(readLockFile).mockResolvedValueOnce(baseLockFile);
189188
vi.mocked(clack.select).mockResolvedValueOnce("sensible-defaults");
190189
vi.mocked(clack.multiselect).mockResolvedValueOnce(["cursor"]);
191190

packages/cli/src/commands/configure.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,7 @@ export async function runConfigure(
8080
: `${w.description} · unverified — config generation may be inaccurate`
8181
}));
8282

83-
const savedHarnesses = lockFile.harnesses;
84-
const initialHarnesses = savedHarnesses
85-
? savedHarnesses.filter((h) => harnessWriters.some((w) => w.id === h))
86-
: await detectHarnesses(projectRoot, harnessWriters);
83+
const initialHarnesses = await detectHarnesses(projectRoot, harnessWriters);
8784

8885
const selectedHarnesses = await clack.multiselect({
8986
message:

packages/cli/src/commands/install.integration.spec.ts

Lines changed: 4 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -40,37 +40,21 @@ describe("install integration (real temp dir)", () => {
4040
await rm(dir, { recursive: true, force: true });
4141
});
4242

43-
it("applies lock file to regenerate agent files without re-resolving", async () => {
43+
it("completes without error after setup", async () => {
4444
const catalog = getDefaultCatalog();
4545

46-
// Step 1: Run setup to create config.yaml + config.lock.yaml
4746
vi.mocked(clack.select)
4847
.mockResolvedValueOnce("codemcp-workflows") // process
4948
.mockResolvedValueOnce("other"); // architecture
5049
vi.mocked(clack.multiselect).mockResolvedValueOnce([]); // practices: none
5150
await runSetup(dir, catalog);
5251

53-
// Step 2: Run install — writes agent files from lock file
54-
await runInstall(dir, ["claude-code"]);
55-
56-
// Agent files should be written by install
57-
const agentMd = await readFile(
58-
join(dir, ".claude", "agents", "ade.md"),
59-
"utf-8"
60-
);
61-
expect(agentMd).toContain("Call whats_next()");
62-
63-
const mcpJson = JSON.parse(await readFile(join(dir, ".mcp.json"), "utf-8"));
64-
expect(mcpJson.mcpServers["workflows"]).toMatchObject({
65-
command: "npx",
66-
args: ["@codemcp/workflows-server@latest"]
67-
});
52+
await runInstall(dir);
6853
});
6954

7055
it("does not modify the lock file", async () => {
7156
const catalog = getDefaultCatalog();
7257

73-
// Setup first
7458
vi.mocked(clack.select)
7559
.mockResolvedValueOnce("codemcp-workflows") // process
7660
.mockResolvedValueOnce("other"); // architecture
@@ -82,37 +66,15 @@ describe("install integration (real temp dir)", () => {
8266
"utf-8"
8367
);
8468

85-
// Re-install
86-
await runInstall(dir, ["claude-code"]);
69+
await runInstall(dir);
8770

8871
const lockRawAfter = await readFile(join(dir, "config.lock.yaml"), "utf-8");
89-
// Lock file should be byte-identical (install doesn't rewrite it)
9072
expect(lockRawAfter).toBe(lockRawBefore);
9173
});
9274

9375
it("fails when no config.lock.yaml exists", async () => {
94-
await expect(runInstall(dir, ["claude-code"])).rejects.toThrow(
76+
await expect(runInstall(dir)).rejects.toThrow(
9577
/config\.lock\.yaml not found/i
9678
);
9779
});
98-
99-
it("works with native-agents-md option", async () => {
100-
const catalog = getDefaultCatalog();
101-
102-
// Setup with native-agents-md
103-
vi.mocked(clack.select)
104-
.mockResolvedValueOnce("native-agents-md") // process
105-
.mockResolvedValueOnce("other"); // architecture
106-
vi.mocked(clack.multiselect).mockResolvedValueOnce([]); // practices: none
107-
await runSetup(dir, catalog);
108-
109-
// Run install
110-
await runInstall(dir, ["claude-code"]);
111-
112-
const agentMd = await readFile(
113-
join(dir, ".claude", "agents", "ade.md"),
114-
"utf-8"
115-
);
116-
expect(agentMd).toContain("AGENTS.md");
117-
});
11880
});

packages/cli/src/commands/install.spec.ts

Lines changed: 18 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import type { LogicalConfig } from "@codemcp/ade-core";
66
vi.mock("@clack/prompts", () => ({
77
intro: vi.fn(),
88
outro: vi.fn(),
9+
confirm: vi.fn().mockResolvedValue(true),
10+
cancel: vi.fn(),
911
log: { info: vi.fn(), warn: vi.fn(), error: vi.fn() }
1012
}));
1113

@@ -27,131 +29,42 @@ vi.mock("@codemcp/ade-core", async (importOriginal) => {
2729
};
2830
});
2931

30-
const mockInstall = vi.hoisted(() => vi.fn().mockResolvedValue(undefined));
31-
3232
vi.mock("@codemcp/ade-harnesses", () => ({
33-
allHarnessWriters: [
34-
{
35-
id: "universal",
36-
label: "Universal",
37-
description: "Universal",
38-
install: mockInstall
39-
},
40-
{
41-
id: "claude-code",
42-
label: "Claude Code",
43-
description: "Claude Code",
44-
install: mockInstall
45-
},
46-
{
47-
id: "cursor",
48-
label: "Cursor",
49-
description: "Cursor",
50-
install: mockInstall
51-
}
52-
],
53-
getHarnessWriter: vi.fn().mockImplementation((id: string) => {
54-
if (id === "universal" || id === "claude-code" || id === "cursor") {
55-
return { id, install: mockInstall };
56-
}
57-
return undefined;
58-
}),
59-
getHarnessIds: vi
60-
.fn()
61-
.mockReturnValue([
62-
"universal",
63-
"claude-code",
64-
"cursor",
65-
"copilot",
66-
"windsurf",
67-
"cline",
68-
"roo-code",
69-
"kiro",
70-
"opencode"
71-
]),
7233
installSkills: vi.fn().mockResolvedValue(undefined),
7334
writeInlineSkills: vi.fn().mockResolvedValue([])
7435
}));
7536

37+
vi.mock("../knowledge-installer.js", () => ({
38+
installKnowledge: vi.fn().mockResolvedValue(undefined)
39+
}));
40+
7641
import * as clack from "@clack/prompts";
7742
import { readLockFile } from "@codemcp/ade-core";
7843
import { runInstall } from "./install.js";
7944

8045
// ── Tests ────────────────────────────────────────────────────────────────────
8146

47+
const baseLockFile = {
48+
version: 1 as const,
49+
generated_at: "2024-01-01T00:00:00.000Z",
50+
choices: { process: "codemcp-workflows" },
51+
logical_config: mockLogical
52+
};
53+
8254
describe("runInstall", () => {
83-
beforeEach(async () => {
55+
beforeEach(() => {
8456
vi.clearAllMocks();
85-
// Re-set the default implementation after clearAllMocks
86-
const { getHarnessWriter } = await import("@codemcp/ade-harnesses");
87-
vi.mocked(getHarnessWriter).mockImplementation((id: string) => {
88-
if (id === "universal" || id === "claude-code" || id === "cursor") {
89-
return {
90-
id,
91-
label: id,
92-
description: "test",
93-
install: mockInstall
94-
};
95-
}
96-
return undefined;
97-
});
57+
vi.mocked(clack.confirm).mockResolvedValue(true);
9858
});
9959

100-
it("reads config.lock.yaml and applies logical config", async () => {
101-
vi.mocked(readLockFile).mockResolvedValueOnce({
102-
version: 1,
103-
generated_at: "2024-01-01T00:00:00.000Z",
104-
choices: { process: "codemcp-workflows" },
105-
logical_config: mockLogical
106-
});
60+
it("reads config.lock.yaml", async () => {
61+
vi.mocked(readLockFile).mockResolvedValueOnce(baseLockFile);
10762

10863
await runInstall("/tmp/project");
10964

11065
expect(readLockFile).toHaveBeenCalledWith("/tmp/project");
11166
});
11267

113-
it("defaults to universal harness when none specified", async () => {
114-
vi.mocked(readLockFile).mockResolvedValueOnce({
115-
version: 1,
116-
generated_at: "2024-01-01T00:00:00.000Z",
117-
choices: { process: "codemcp-workflows" },
118-
logical_config: mockLogical
119-
});
120-
121-
await runInstall("/tmp/project");
122-
123-
expect(mockInstall).toHaveBeenCalledWith(mockLogical, "/tmp/project");
124-
});
125-
126-
it("uses harnesses from lock file when present", async () => {
127-
vi.mocked(readLockFile).mockResolvedValueOnce({
128-
version: 1,
129-
generated_at: "2024-01-01T00:00:00.000Z",
130-
choices: { process: "codemcp-workflows" },
131-
harnesses: ["claude-code", "cursor"],
132-
logical_config: mockLogical
133-
});
134-
135-
await runInstall("/tmp/project");
136-
137-
expect(mockInstall).toHaveBeenCalledTimes(2);
138-
});
139-
140-
it("uses explicit harness ids when provided", async () => {
141-
vi.mocked(readLockFile).mockResolvedValueOnce({
142-
version: 1,
143-
generated_at: "2024-01-01T00:00:00.000Z",
144-
choices: { process: "codemcp-workflows" },
145-
harnesses: ["claude-code"],
146-
logical_config: mockLogical
147-
});
148-
149-
await runInstall("/tmp/project", ["cursor"]);
150-
151-
// Explicit takes priority over lock file
152-
expect(mockInstall).toHaveBeenCalledTimes(1);
153-
});
154-
15568
it("throws when config.lock.yaml is missing", async () => {
15669
vi.mocked(readLockFile).mockResolvedValueOnce(null);
15770

@@ -160,26 +73,8 @@ describe("runInstall", () => {
16073
);
16174
});
16275

163-
it("throws when harness id is unknown", async () => {
164-
vi.mocked(readLockFile).mockResolvedValueOnce({
165-
version: 1,
166-
generated_at: "2024-01-01T00:00:00.000Z",
167-
choices: { process: "codemcp-workflows" },
168-
logical_config: mockLogical
169-
});
170-
171-
await expect(runInstall("/tmp/project", ["unknown-agent"])).rejects.toThrow(
172-
/unknown harness/i
173-
);
174-
});
175-
17676
it("shows intro and outro messages", async () => {
177-
vi.mocked(readLockFile).mockResolvedValueOnce({
178-
version: 1,
179-
generated_at: "2024-01-01T00:00:00.000Z",
180-
choices: { process: "codemcp-workflows" },
181-
logical_config: mockLogical
182-
});
77+
vi.mocked(readLockFile).mockResolvedValueOnce(baseLockFile);
18378

18479
await runInstall("/tmp/project");
18580

packages/cli/src/commands/install.ts

Lines changed: 2 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,18 @@
11
import * as clack from "@clack/prompts";
22
import { readLockFile } from "@codemcp/ade-core";
3-
import {
4-
type HarnessWriter,
5-
allHarnessWriters,
6-
getHarnessWriter,
7-
getHarnessIds,
8-
detectHarnesses,
9-
installSkills,
10-
writeInlineSkills
11-
} from "@codemcp/ade-harnesses";
3+
import { installSkills, writeInlineSkills } from "@codemcp/ade-harnesses";
124
import { installKnowledge } from "../knowledge-installer.js";
135

14-
export async function runInstall(
15-
projectRoot: string,
16-
harnessIds?: string[],
17-
harnessWriters: HarnessWriter[] = allHarnessWriters
18-
): Promise<void> {
6+
export async function runInstall(projectRoot: string): Promise<void> {
197
clack.intro("ade install");
208

219
const lockFile = await readLockFile(projectRoot);
2210
if (!lockFile) {
2311
throw new Error("config.lock.yaml not found. Run `ade setup` first.");
2412
}
2513

26-
// Determine which harnesses to install for:
27-
// 1. --harness flag (comma-separated)
28-
// 2. harnesses saved in the lock file
29-
// 3. auto-detected from project artifacts (falls back to universal if none found)
30-
const ids =
31-
harnessIds ??
32-
lockFile.harnesses ??
33-
(await detectHarnesses(projectRoot, harnessWriters).then((detected) =>
34-
detected.length > 0 ? detected : ["universal"]
35-
));
36-
37-
const validIds = [...getHarnessIds(), ...harnessWriters.map((w) => w.id)];
38-
const uniqueValidIds = [...new Set(validIds)];
39-
for (const id of ids) {
40-
if (!uniqueValidIds.includes(id)) {
41-
throw new Error(
42-
`Unknown harness "${id}". Available: ${uniqueValidIds.join(", ")}`
43-
);
44-
}
45-
}
46-
4714
const logicalConfig = lockFile.logical_config;
4815

49-
for (const id of ids) {
50-
const writer =
51-
harnessWriters.find((w) => w.id === id) ?? getHarnessWriter(id);
52-
if (writer) {
53-
await writer.install(logicalConfig, projectRoot);
54-
}
55-
}
56-
5716
const modifiedSkills = await writeInlineSkills(logicalConfig, projectRoot);
5817
if (modifiedSkills.length > 0) {
5918
clack.log.warn(

packages/cli/src/commands/knowledge-docset.integration.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ describe("knowledge docset regression tests", () => {
133133
await rm(join(dir, ".knowledge"), { recursive: true, force: true });
134134

135135
// Now run install — should also write .knowledge/config.yaml
136-
await runInstall(dir, ["claude-code"]);
136+
await runInstall(dir);
137137

138138
// All 4 tanstack docsets are configured via the docset writer
139139
expect(createDocset).toHaveBeenCalledTimes(4);

0 commit comments

Comments
 (0)