Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions aidd_docs/memory/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ The `aidd` binary, built from `cli/` and published to npm as `@ai-driven-dev/cli

## Commands

- `setup --provider` chooses the provider the main session and each IDE's agents run on, machine-wide: [agent guide](../../docs/SUBAGENTS.md#-models).
- `telemetry` counts the tokens of Claude Code and Codex sessions locally: [usage contract](../product/usage-contract.md).
- No list here. The surface moves; `aidd --help` is the only reading that stays true.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ Behavior and sources are in [the agent guide](../../../../docs/SUBAGENTS.md).

- **Agents:** explorer, executor, validator and reviewer ship in `plugins/aidd-orchestrator/agents/`, installed with the plugin. No tool list is declared: every host gives an agent all tools by default.
- **Translation:** each host receives only the keys its documentation lists: name or file path as the ID, description, model, and Claude's `effort` under the host's own key. Each host has one agent frontmatter function, shared by the plugin install and its built distributions.
- **Out of scope:** per-provider models and a model for the main session.
- **Models:** `aidd setup --provider` records a provider per IDE; every build that installs agents gives each role its tier's model from `agent-models.json`, and the main session's model goes in the IDE's user settings.

## Checks

- **Claude Code:** the four agents load and run from a project-scoped plugin install.
- **OpenCode:** the four agents run from the flat distribution through `opencode run --standalone --agent <id>` with an OpenAI model, each answering after a `read` call.
- **Codex:** the four agents load from the flat distribution in a trusted project; `codex exec` spawned each, and its session records the agent's role and instructions.
- **Provider models:** a built OpenCode agent with `openai` loaded as `{id: gpt-6-luna, providerID: openai, variant: medium}` (`opencode debug agents`), and a Codex session spawned the explorer on `gpt-6-luna` while its parent ran `gpt-6.1-sol`.
- **Setter:** in a scratch project, `aidd setup --provider anthropic` gave the reviewer `claude-opus-5-5` and wrote the main session's `claude-sonnet-5-5` in the user settings of Claude Code and OpenCode; `doctor` stayed healthy and `sync` passed; `--provider none` removed every model.
- **Every host:** `cli/tests/e2e/agent-install.e2e.test.ts` builds the repository flat and checks each shipped agent's path, frontmatter and body against the host's documented format.
- **Copilot, Cursor, Kilo:** no host binary in the test environment, so loading by the host is unverified. The Copilot plugin lists agent files where its documentation describes agent directories.
8 changes: 7 additions & 1 deletion cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,15 @@ Check: `aidd --version`.

`setup` writes the manifest, the default marketplace, the tool configs and the plugins.
`--global` installs for the machine instead, and writes nothing under the project.
`--provider` runs the main session and the framework's agents on that provider's models, reports which model each runs on, and `--provider none` undoes it.

The model ids are pinned in [`agent-models.json`](src/contexts/tools/domain/formats/models/agent-models.json), checked against each provider's model page on 2026-11-04. When a provider ships or retires a model, update that file, the table in [the agent guide](../docs/SUBAGENTS.md#-models), and the tests that name the ids.

| Command | Lands in |
| --- | --- |
| `aidd setup --ide claude` | `.claude/` and `.aidd/` in this project |
| `aidd setup --global --ide opencode --plugins all` | `~/.config/opencode/` and `~/.config/aidd/`, nothing in this project |
| `aidd setup --ide opencode --provider mistral` | `.opencode/agents/` with Mistral models; the main session's in `~/.config/opencode/opencode.jsonc`; the provider in `~/.config/aidd/agent-models.json`, for every project |

## Commands

Expand Down Expand Up @@ -139,9 +143,11 @@ Install it, then rerun the original `aidd setup --global` command with its `--id
```sh
aidd setup --global --ide claude,codex,copilot,cursor,kilo,opencode --plugins all --yes
aidd sync --global # rebuild the copied files and refresh the hosts from the framework source
aidd clean --global --force # remove what --global installed and purge the shared source; without --force it previews or asks
aidd clean --global --force # remove what --global installed, purge the shared source, forget the provider and restore the main session's model; without --force it previews or asks
```

With `--provider`, the copied agents and enabled plugins carry that provider's models.

## Authentication

Needed for a private marketplace only.
Expand Down
2 changes: 2 additions & 0 deletions cli/aidd_docs/memory/codebase-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ src/
│ │ └── infrastructure/
│ ├── framework/ # the installation record and everything done to a project
│ │ ├── application/
│ │ │ ├── agents/ # which model each host's agents run on
│ │ │ ├── clean/
│ │ │ ├── doctor/
│ │ │ ├── flows/
Expand All @@ -39,6 +40,7 @@ src/
│ │ ├── domain/
│ │ │ ├── capabilities/
│ │ │ ├── formats/
│ │ │ │ └── models/ # provider models for agents and the main session's settings
│ │ │ ├── models/
│ │ │ ├── ports/
│ │ │ └── profiles/ # one directory per tool
Expand Down
1 change: 1 addition & 0 deletions cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
"ajv": "^8.20.0",
"ajv-formats": "^3.0.1",
"commander": "^15.0.0",
"jsonc-parser": "^3.3.1",
"simple-git": "^4.0.2",
"smol-toml": "^1.6.1"
},
Expand Down
8 changes: 8 additions & 0 deletions cli/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions cli/scripts/smoke-tools.sh
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,13 @@ run "telemetry forget (preview)" 0 "Nothing was removed" "$P_TEL" -- telemetry f
run "telemetry forget --yes" 0 "Removed" "$P_TEL" -- telemetry forget --yes
run "telemetry off (after forget)" 0 "Measurement was not on" "$P_TEL" -- telemetry off

# The provider lands in the sandboxed $AIDD_USER_CONFIG_DIR, never the real profile's.
section "setup --provider (sandboxed profile)"
P_AGENTS=$(new_project)
SETUP_OC=(setup --source local --path "$FRAMEWORK_FIXTURE" --ide opencode --plugins none --yes)
run "setup --provider" 0 "Models on mistral" "$P_AGENTS" -- "${SETUP_OC[@]}" --provider mistral
run "setup --provider none" 0 "run on the IDE's model again" "$P_AGENTS" -- "${SETUP_OC[@]}" --provider none

section "update --check"
out=$(cd "$ROOT" && node "$CLI" update --check 2>&1); rc=$?
if [[ "$rc" -eq 0 || "$rc" -eq 1 ]]; then mark_covered "update"; ok "update --check (exit $rc)"; else bad "update crashed (exit $rc)" "$out"; fi
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
import { join } from "node:path";
import type { FileReader } from "../../../../kernel/ports/file-reader.js";
import type { FileWriter } from "../../../../kernel/ports/file-writer.js";
import type { MarketplaceScope } from "../../../../kernel/scope.js";
import type { EnvironmentReader } from "../../../tools/domain/capabilities/plugins-capability.js";
import {
AGENT_MODELS,
type AgentHost,
type AgentProvider,
supportedProvider,
} from "../../../tools/domain/formats/models/agent-models.js";
import {
mainModelValues,
planMainModelInstall,
planMainModelRemoval,
} from "../../../tools/domain/formats/models/main-model-config.js";
import { globalFilesOf, userMachineLocalFilesOf } from "../../../tools/domain/registry.js";
import type { AgentModelStore } from "../../domain/ports/agent-model-store.js";
import type { ManifestRepository } from "../../domain/ports/manifest-repository.js";
import type { MarketplaceSyncSettings } from "../flows/marketplace-sync-settings-use-case.js";
import type { PluginUpdateUseCase } from "../plugin/plugin-update-use-case.js";

export interface SetAgentModelOptions {
readonly projectRoot: string;
readonly hosts: readonly AgentHost[];
/** Absent, the hosts go back to the IDE's model. */
readonly provider?: string;
/** `"user"` also refreshes what `setup --global` installed; the provider and the main
* session's model hold for the whole machine either way. */
readonly scope: MarketplaceScope;
}

export interface SetAgentModelResult {
/** Plugins this project re-installed with the new models. */
readonly reinstalled: readonly string[];
/** Each host whose main session's settings file this run changed, and that file. */
readonly changed: readonly { readonly host: AgentHost; readonly path: string }[];
/** Each host whose settings file was left as it is, and why. */
readonly skipped: readonly {
readonly host: AgentHost;
readonly path: string;
readonly reason: string;
}[];
}

interface FileChange {
readonly host: AgentHost;
readonly path: string;
readonly content: string | null;
}

/**
* The setter for agent models, run once the install has succeeded. Records the provider each
* host's agents run on, which every later build of that host applies, re-installs this
* project's plugins so they carry it, then points the host's main session at the provider's
* main-tier model in the host's own user settings, machine-wide like the provider and never a
* file a project tracks, recording what that replaced so unsetting restores it.
*/
export class SetAgentModelUseCase {
constructor(
private readonly fs: FileReader & FileWriter,
private readonly store: AgentModelStore,
private readonly manifestRepo: ManifestRepository,
private readonly userManifestRepo: ManifestRepository,
private readonly pluginUpdate: Pick<PluginUpdateUseCase, "execute">,
private readonly userPluginUpdate: Pick<PluginUpdateUseCase, "execute">,
private readonly activation: MarketplaceSyncSettings,
private readonly homedir: () => string,
private readonly env: EnvironmentReader
) {}

async execute(options: SetAgentModelOptions): Promise<SetAgentModelResult> {
const chosen = this.chosenProviders(options);
const before = await this.store.loadProviders();
const providers = { ...before };
for (const host of options.hosts) {
const provider = chosen.get(host);
if (provider === undefined) delete providers[host];
else providers[host] = provider;
}
await this.store.saveProviders(providers);
let reinstalled: readonly string[];
try {
reinstalled = await this.reinstall(options);
} catch (error) {
// A rebuild that failed leaves the machine on the provider it had.
await this.store.saveProviders(before);
throw error;
}
return { reinstalled, ...(await this.applyMainModels(options.hosts, chosen)) };
}

/** `clean --global`: forgets every host's provider and gives each main session back what it
* held before AIDD, machine-wide. `dryRun` says what it would restore and changes nothing. */
async reset(options: {
readonly dryRun: boolean;
}): Promise<
Pick<SetAgentModelResult, "changed" | "skipped"> & { readonly providers: readonly AgentHost[] }
> {
const providers = Object.keys(await this.store.loadProviders()) as AgentHost[];
const hosts = Object.keys(await this.store.loadMainModels()) as AgentHost[];
const restored = await this.applyMainModels(hosts, new Map(), options.dryRun);
if (!options.dryRun) await this.store.saveProviders({});
return { ...restored, providers };
}

/** Points each host's main session at the provider, or restores what it replaced once none
* is chosen. A settings file it cannot safely rewrite is skipped, never half-written. */
private async applyMainModels(
hosts: readonly AgentHost[],
chosen: ReadonlyMap<AgentHost, AgentProvider>,
dryRun = false
): Promise<Pick<SetAgentModelResult, "changed" | "skipped">> {
const mainModels = await this.store.loadMainModels();
const changes: FileChange[] = [];
const skipped: { host: AgentHost; path: string; reason: string }[] = [];
for (const host of hosts) {
const provider = chosen.get(host);
const record = mainModels[host];
const current = await this.settingsPath(host);
let path = record?.path ?? current;
try {
// Restored where it was written, once unset or once the host reads another file.
if (record !== undefined && (provider === undefined || record.path !== current)) {
const content = await this.readIfAny(record.path);
const next = planMainModelRemoval(content, record);
delete mainModels[host];
if (next !== content) changes.push({ host, path: record.path, content: next });
}
if (provider === undefined || current === undefined) continue;
path = current;
const plan = planMainModelInstall(
current,
await this.readIfAny(current),
mainModelValues(host, provider),
mainModels[host],
AGENT_MODELS.hosts[host].main?.schema
);
mainModels[host] = plan.record;
changes.push({ host, path: current, content: plan.content });
} catch (error) {
if (path !== undefined) skipped.push({ host, path, reason: (error as Error).message });
}
}
const result = { changed: changes.map(({ host, path }) => ({ host, path })), skipped };
if (dryRun) return result;
// The record goes first: a run stopped between two settings files still knows what each
// held before, so unsetting restores it instead of taking AIDD's value for the person's.
await this.store.saveMainModels(mainModels);
for (const { path, content } of changes) {
if (content === null) await this.fs.deleteFile(path);
else await this.fs.writeFile(path, content);
}
return result;
}

private async readIfAny(path: string): Promise<string | null> {
return (await this.fs.fileExists(path)) ? this.fs.readFile(path) : null;
}

private chosenProviders(
options: Pick<SetAgentModelOptions, "hosts" | "provider">
): Map<AgentHost, AgentProvider> {
const chosen = new Map<AgentHost, AgentProvider>();
if (options.provider !== undefined) {
for (const host of options.hosts) chosen.set(host, supportedProvider(host, options.provider));
}
return chosen;
}

/** The host's own user settings file; a host declaring none there reads `main.file`, a JSONC
* file that outranks its JSON twin, from the directory a machine-wide install copies its files
* into. The twin is used only when it alone exists. Absent where neither exists. */
private async settingsPath(host: AgentHost): Promise<string | undefined> {
const settings = AGENT_MODELS.hosts[host].main;
if (settings === undefined) return undefined;
const home = this.homedir();
const own = userMachineLocalFilesOf(host, home, this.env)[0];
if (own !== undefined) return own;
const globalDir = globalFilesOf(host)?.dir(home);
if (globalDir === undefined) return undefined;
const path = join(globalDir, settings.file);
const twin = path.replace(/c$/, "");
return !(await this.fs.fileExists(path)) && (await this.fs.fileExists(twin)) ? twin : path;
Comment thread
blafourcade marked this conversation as resolved.
Resolved
}

/** Files a plugin wrote are rewritten from the rebuilt tree, and a host that installs plugins
* itself is driven again, which hands it that tree. The provider is machine-wide, so this
* project is always refreshed, and a plugin installed in the user's home, as Cursor's always
* is, too; `--global` adds the machine-scope registrations. */
private async reinstall(options: SetAgentModelOptions): Promise<readonly string[]> {
const rewritten = new Set<string>();
const userManifest = await this.userManifestRepo.load();
const userPlugins = options.hosts.filter((host) =>
userManifest?.getPlugins(host).some((plugin) => plugin.scope === "user")
);
if (userPlugins.length > 0) {
const names = await this.userPluginUpdate.execute({
toolIds: userPlugins,
projectRoot: options.projectRoot,
scope: "user",
rebuild: true,
});
for (const name of names) rewritten.add(name);
}
const manifest = await this.manifestRepo.load();
const installed = options.hosts.filter((host) => manifest?.hasTool(host) === true);
const projectPlugins = installed.filter((host) =>
manifest?.getPlugins(host).some((plugin) => plugin.scope !== "user")
);
if (projectPlugins.length > 0) {
const names = await this.pluginUpdate.execute({
toolIds: projectPlugins,
projectRoot: options.projectRoot,
rebuild: true,
});
for (const name of names) rewritten.add(name);
}
if (installed.length > 0) await this.activate(options.projectRoot, installed);
Comment thread
blafourcade marked this conversation as resolved.
Resolved
if (options.scope === "user") {
const machine = options.hosts.filter((host) => userManifest?.hasTool(host) === true);
if (machine.length > 0) await this.activate(options.projectRoot, machine, true);
}
return [...rewritten];
}

private async activate(projectRoot: string, toolIds: AgentHost[], user = false): Promise<void> {
const { errors } = await this.activation.execute({
projectRoot,
toolIds,
...(user ? { scope: "user" as const, manifestRepo: this.userManifestRepo } : {}),
});
if (errors.length > 0) throw new Error(errors.map((error) => error.message).join("\n"));
}
}
Loading
Loading