Skip to content
Draft
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 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,15 @@ 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`.
- **Out of scope:** plugin installs by `aidd setup --global`, so `make reload` applies no model.

## 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 mistral` ran the OpenCode reviewer on `mistral-medium-latest`, and `--provider anthropic` the Claude reviewer on `claude-sonnet-5-5`; `--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.
4 changes: 4 additions & 0 deletions cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,13 @@ 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 framework's agents on that provider's models, and `--provider none` undoes it.

| 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 provider in `~/.config/aidd/agent-models.json`, for every project |

## Commands

Expand Down Expand Up @@ -138,6 +140,8 @@ aidd sync --global # rebuild the copied files and refresh the hosts from the
aidd clean --global # remove what --global installed and purge the shared source
```

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

## Authentication

Needed for a private marketplace only.
Expand Down
1 change: 1 addition & 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 Down
2 changes: 1 addition & 1 deletion cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"node": ">=22.12"
},
"packageManager": "pnpm@12.16.1",
"bundleBudgetKB": 734,
"bundleBudgetKB": 741,
"scripts": {
"build": "tsup && node scripts/check-bundle-size.mjs",
"build:check-size": "node scripts/check-bundle-size.mjs",
Expand Down
2 changes: 2 additions & 0 deletions cli/scripts/check-bundle-size.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ const pkg = JSON.parse(readFileSync(resolve(root, "package.json"), "utf8"));
// 0.2% measured headroom.
// 734 KB: 718.8 KB, the machine-wide install of `setup --global` (+8.0 KB over the 710.8 KB
// above); 2.1% measured headroom.
// 741 KB: 725.6 KB, the agents' models: a provider per IDE that every install build applies
// (+6.8 KB over the 718.8 KB above); 2.1% measured headroom.
const budgetKB = pkg.bundleBudgetKB ?? 500;
const budgetBytes = budgetKB * 1024;

Expand Down
9 changes: 8 additions & 1 deletion cli/scripts/smoke-tools.sh
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ project_tree_hash() {
' )
}

PARENTS=" plugin marketplace auth framework telemetry "
PARENTS=" plugin marketplace auth framework telemetry agents "
# A parent one level deeper than PARENTS, so a covered key needs three words there, not two.
GRANDPARENTS=" "
derive_key() {
Expand Down 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 "run on mistral" "$P_AGENTS" -- "${SETUP_OC[@]}" --provider mistral
run "setup --provider none" 0 "session's model" "$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,104 @@
import type { MarketplaceScope } from "../../../../kernel/scope.js";
import {
type AgentHost,
type AgentProvider,
supportedProvider,
} from "../../../tools/domain/formats/agent-models.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' agents go back to the session's model. */
readonly provider?: string;
/** `"user"` also refreshes what `setup --global` installed for the machine. */
readonly scope: MarketplaceScope;
}

export interface SetAgentModelResult {
/** Plugins re-installed with the new models. */
readonly reinstalled: readonly string[];
}

/**
* The setter for agent models. Records the provider each host's agents run on, which every
* build of that host then applies, and re-installs the plugins already installed so they
* carry it. The session's own model stays the IDE's choice.
*/
export class SetAgentModelUseCase {
constructor(
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
) {}

async execute(options: SetAgentModelOptions): Promise<SetAgentModelResult> {
const chosen = new Map<AgentHost, AgentProvider>();
if (options.provider !== undefined) {
for (const host of options.hosts) chosen.set(host, supportedProvider(host, options.provider));
}
const providers = await this.store.loadProviders();
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);
return { reinstalled: await this.reinstall(options) };
}

/** 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);
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"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@ export interface PluginUpdateOptions {
toolIds: AiToolId[] | "all";
projectRoot: string;
scope?: "project" | "user";
/** Re-materialize at the same version: the source changed, not the plugin's version. Read by
* the machine-scope update, which `sync --global` drives. */
/** Re-materialize at the same version: the build changed, not the plugin. */
rebuild?: boolean;
Comment thread
alexsoyes marked this conversation as resolved.
Resolved
}

Expand Down Expand Up @@ -60,7 +59,8 @@ export class PluginUpdateUseCase {
pluginNames,
projectRoot,
cacheDir,
manifest
manifest,
options.rebuild === true
);
updated.push(...names);
}
Expand All @@ -73,15 +73,23 @@ export class PluginUpdateUseCase {
pluginNames: string[] | undefined,
projectRoot: string,
cacheDir: string,
manifest: Manifest
manifest: Manifest,
rebuild: boolean
): Promise<string[]> {
const plugins = manifest.getPlugins(toolId);
const targets = pluginNames
? plugins.filter((p) => pluginNames.includes(p.name))
: plugins.filter((plugin) => plugin.scope !== "user");
const updated: string[] = [];
for (const plugin of targets) {
const didUpdate = await this.updateOnePlugin(plugin, toolId, projectRoot, cacheDir, manifest);
const didUpdate = await this.updateOnePlugin(
plugin,
toolId,
projectRoot,
cacheDir,
manifest,
rebuild
);
if (didUpdate) updated.push(plugin.name);
}
return updated;
Expand All @@ -92,14 +100,15 @@ export class PluginUpdateUseCase {
toolId: AiToolId,
projectRoot: string,
cacheDir: string,
manifest: Manifest
manifest: Manifest,
rebuild: boolean
): Promise<boolean> {
if (plugin.scope === "user") throw new InvalidPluginScopeError(toolId, "project", "user");
const dist = await this.distributionLoader.load(plugin.source, cacheDir, {
forceRefresh: true,
});
await this.pluginRuntime?.ensurePluginRuntimeFiles(toolId, projectRoot, manifest);
if (compareSemver(dist.manifest.version, plugin.version) <= 0) return false;
if (!rebuild && compareSemver(dist.manifest.version, plugin.version) <= 0) return false;
await this.replacePluginFiles(plugin, dist, toolId, projectRoot, manifest);
return true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,18 @@ import type { FrameworkBuildMode } from "../../../tools/domain/registry.js";
import type { FrameworkBuild } from "../../../translate/application/translate-source.js";
import type { FrameworkBuildTarget } from "../../../translate/domain/build-target.js";

/** `undefined` when the target/mode pair has no build. */
/** `undefined` when the target/mode pair has no build. With a provider, every agent gets its
* role's model there. */
export type FrameworkBuildFor = (
target: FrameworkBuildTarget,
mode: FrameworkBuildMode,
outDir: string
outDir: string,
provider?: string
) => FrameworkBuild | undefined;

/** The provider chosen for a target's agents, if any. */
export type AgentProviderFor = (target: FrameworkBuildTarget) => Promise<string | undefined>;

export interface EnsureBuiltMarketplaceOptions {
readonly projectRoot: string;
readonly marketplace: Marketplace;
Expand Down Expand Up @@ -60,7 +65,9 @@ export class EnsureBuiltMarketplaceUseCase implements EnsureBuiltMarketplace {
* happened to register it would tie a declaration meant for every project to the life of one
* of them: delete that project and the global registration points at nothing.
*/
private readonly userCacheRoot: () => string
private readonly userCacheRoot: () => string,
/** A changed provider changes the built agents, so it is part of what keeps a cache fresh. */
private readonly agentProviderFor: AgentProviderFor = async () => undefined
) {}

async execute(options: EnsureBuiltMarketplaceOptions): Promise<EnsureBuiltMarketplaceResult> {
Expand All @@ -82,7 +89,8 @@ export class EnsureBuiltMarketplaceUseCase implements EnsureBuiltMarketplace {
projectRoot: options.projectRoot,
forceRefresh: options.forceRefresh,
});
const sentinel = this.sentinelValue(resolved.catalog?.version);
const provider = await this.agentProviderFor(options.target);
const sentinel = this.sentinelValue(resolved.catalog?.version, provider);
const memoKey = `${options.marketplace.name}:${options.target}:${sentinel}`;
const memoized = this.memo.get(memoKey);
if (memoized !== undefined) return memoized;
Expand All @@ -91,14 +99,16 @@ export class EnsureBuiltMarketplaceUseCase implements EnsureBuiltMarketplace {
builtDir,
resolve(resolved.localPath),
sentinel,
this.versionIsTrustworthy(options)
this.versionIsTrustworthy(options),
provider
);
this.memo.set(memoKey, result);
return result;
}

private sentinelValue(catalogVersion: string | undefined): string {
return `${this.version.get()}:${catalogVersion ?? UNVERSIONED}`;
private sentinelValue(catalogVersion: string | undefined, provider: string | undefined): string {
const base = `${this.version.get()}:${catalogVersion ?? UNVERSIONED}`;
return provider === undefined ? base : `${base}:${provider}`;
}

/**
Expand All @@ -120,13 +130,14 @@ export class EnsureBuiltMarketplaceUseCase implements EnsureBuiltMarketplace {
builtDir: string,
sourceDir: string,
sentinel: string,
versionIsTrustworthy: boolean
versionIsTrustworthy: boolean,
provider: string | undefined
): Promise<EnsureBuiltMarketplaceResult> {
const version = sentinel.split(":")[1];
if (versionIsTrustworthy && (await this.isFresh(builtDir, sentinel))) {
return { builtDir, version, rebuilt: false };
}
await this.build(options.target, options.mode, sourceDir, builtDir);
await this.build(options.target, options.mode, sourceDir, builtDir, provider);
await this.fs.writeFile(join(builtDir, SENTINEL_FILE), sentinel);
return { builtDir, version, rebuilt: true };
}
Expand All @@ -143,13 +154,14 @@ export class EnsureBuiltMarketplaceUseCase implements EnsureBuiltMarketplace {
target: FrameworkBuildTarget,
mode: FrameworkBuildMode,
sourceDir: string,
builtDir: string
builtDir: string,
provider: string | undefined
): Promise<void> {
if (this.nested(sourceDir, builtDir)) {
await this.buildViaTemp(target, mode, sourceDir, builtDir);
await this.buildViaTemp(target, mode, sourceDir, builtDir, provider);
return;
}
await this.runBuild(target, mode, sourceDir, builtDir);
await this.runBuild(target, mode, sourceDir, builtDir, provider);
}

private nested(sourceDir: string, builtDir: string): boolean {
Expand All @@ -160,11 +172,12 @@ export class EnsureBuiltMarketplaceUseCase implements EnsureBuiltMarketplace {
target: FrameworkBuildTarget,
mode: FrameworkBuildMode,
sourceDir: string,
builtDir: string
builtDir: string,
provider: string | undefined
): Promise<void> {
const temp = join(tmpdir(), `aidd-built-${target}-${mode}`);
await this.fs.deleteDirectory(temp);
await this.runBuild(target, mode, sourceDir, temp);
await this.runBuild(target, mode, sourceDir, temp, provider);
await this.fs.deleteDirectory(builtDir);
await this.copyDir(temp, builtDir);
await this.fs.deleteDirectory(temp);
Expand All @@ -177,10 +190,11 @@ export class EnsureBuiltMarketplaceUseCase implements EnsureBuiltMarketplace {
target: FrameworkBuildTarget,
mode: FrameworkBuildMode,
sourceDir: string,
outDir: string
outDir: string,
provider: string | undefined
): Promise<void> {
await this.fs.createDirectory(outDir);
const build = this.buildFor(target, mode, outDir);
const build = this.buildFor(target, mode, outDir, provider);
if (build === undefined) {
throw new Error(`No framework build for target '${target}' mode '${mode}'.`);
}
Expand Down
Loading
Loading