Skip to content
Merged
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
13 changes: 12 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,23 @@ node dist/src/installer-cli/index.js install --yes \

Installed files (inside agent home):
- `skills/`
- `ica.config.json`
- `ica.config.default.json`
- `ica.workflow.default.json`
- `VERSION`
- `.ica/install-state.json`

Shared ICA config/state (global, cross-agent by default):
- `~/.ica/ica.config.json`
- `~/.ica/ica.workflow.json`
- `~/.ica/tracking.config.json`
- `~/.ica/sources.json`

Agent-home overrides (optional, active runtime only):
- `~/.codex/ica.config.json`
- `~/.claude/ica.config.json`
- `~/.codex/ica.workflow.json`
- `~/.claude/ica.workflow.json`

Project conventions used by skills:
- `.agent/queue/`
- `summaries/`
Expand Down
11 changes: 7 additions & 4 deletions docs/configuration-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ ICA loads `ica.config.json` in this priority order:
1. AgentTask overrides: `workflow:*` / `config:*` fields inside the AgentTask YAML
2. Project config (preferred): `./.ica/config.json`
3. Project config (compat): `./ica.config.json` or `./.<agent-home>/ica.config.json`
4. User config: `~/.<agent-home>/ica.config.json` (for example `~/.claude/ica.config.json`, `~/.codex/ica.config.json`)
5. Defaults: `ica.config.default.json`
4. Active agent-home override: `~/.<agent-home>/ica.config.json` for the current runtime only
5. Shared ICA global config: `~/.ica/ica.config.json` (or `$ICA_STATE_HOME/ica.config.json`)
6. Defaults: `ica.config.default.json`

Notes:
- `<agent-home>` is the tool-specific directory ICA installs into (`.claude`, `.codex`, `.cursor`, etc.).
- Agent-home files are overrides, not the canonical cross-agent default.
- Claude Code also has tool config files that are separate from ICA config:
- Hooks: `~/.claude/settings.json`
- MCP servers: `~/.claude.json`
Expand All @@ -26,8 +28,9 @@ Workflow settings (version bump rules, PR requirements, release automation, auto
1. AgentTask overrides (`workflow.*` inside the AgentTask YAML)
2. Project workflow (preferred): `./.ica/workflow.json`
3. Project workflow (compat): `./ica.workflow.json` or `./.<agent-home>/ica.workflow.json`
4. User workflow: `~/.<agent-home>/ica.workflow.json`
5. Defaults: `ica.workflow.default.json`
4. Active agent-home override: `~/.<agent-home>/ica.workflow.json` for the current runtime only
5. Shared ICA global workflow: `~/.ica/ica.workflow.json` (or `$ICA_STATE_HOME/ica.workflow.json`)
6. Defaults: `ica.workflow.default.json`

### Enable Agent Auto-Merge (Standing Approval)

Expand Down
12 changes: 7 additions & 5 deletions docs/mcp-proxy.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# MCP Proxy (ICA-Owned)

This doc describes the **ICA MCP Proxy**: a local stdio MCP server you register once in your agent runtime, which then mirrors and brokers access to upstream MCP servers defined in `.mcp.json` and/or `$ICA_HOME/mcp-servers.json`.
This doc describes the **ICA MCP Proxy**: a local stdio MCP server you register once in your agent runtime, which then mirrors and brokers access to upstream MCP servers defined in `.mcp.json`, shared ICA config under `$ICA_STATE_HOME`, and the active agent-home override under `$ICA_HOME`.

## Why A Proxy?

Expand All @@ -15,7 +15,8 @@ So the user only registers one MCP server: `ica-mcp-proxy`.

Create one or both of:
- project: `./.mcp.json`
- user: `$ICA_HOME/mcp-servers.json` (or `$ICA_HOME/mcp.json`)
- shared global: `${ICA_STATE_HOME:-$HOME/.ica}/mcp-servers.json` (or `mcp.json`)
- active agent-home override: `$ICA_HOME/mcp-servers.json` (or `$ICA_HOME/mcp.json`)

Format:

Expand All @@ -35,7 +36,7 @@ Format:
```

Precedence:
- default: `.mcp.json` overrides `$ICA_HOME/mcp-servers.json`
- default: `.mcp.json` overrides user config; active agent-home overrides shared global config
- set `ICA_MCP_CONFIG_PREFER_HOME=1` to flip

## Register In Your Agent Runtime
Expand All @@ -61,7 +62,7 @@ Use one of the snippets below based on your runtime. In all cases, the target co

`python3 <ICA_HOME>/skills/mcp-proxy/scripts/mcp_proxy_server.py`

Replace `<ICA_HOME>` with your real agent home (for example, `~/.codex`, `~/.ica`, or a project-local install path).
Replace `<ICA_HOME>` with your real active agent home (for example, `~/.codex` or a project-local install path). Shared ICA config lives under `${ICA_STATE_HOME:-~/.ica}`.

### Codex (`~/.codex/config.toml`)

Expand Down Expand Up @@ -184,7 +185,8 @@ Example:
## Authentication

Tokens are stored locally in:
- `$ICA_HOME/mcp-tokens.json`
- active agent-home override: `$ICA_HOME/mcp-tokens.json`
- fallback shared global: `${ICA_STATE_HOME:-$HOME/.ica}/mcp-tokens.json`

Auth entry points:
- `proxy.auth_start(server, flow?)`
Expand Down
9 changes: 5 additions & 4 deletions src/behaviors/config-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
**MANDATORY:** Use the configuration hierarchy; do not assume defaults.

## Configuration Hierarchy (highest to lowest)
1. Embedded AgentTask overrides
2. Project config: `./ica.config.json` or `./<agent_home>/ica.config.json`
3. User config: `$ICA_HOME/ica.config.json`
4. System defaults: `ica.config.default.json`
1. Embedded AgentTask overrides
2. Project config: `./.ica/config.json` (compat: `./ica.config.json` or `./<agent_home>/ica.config.json`)
3. Active agent-home override: `$ICA_HOME/ica.config.json`
4. Shared global config: `${ICA_STATE_HOME:-$HOME/.ica}/ica.config.json`
5. System defaults: `ica.config.default.json`

## Key Settings
- `git.*` (privacy, branch protection, PR requirement)
Expand Down
102 changes: 92 additions & 10 deletions src/installer-core/executor.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os from "node:os";
import path from "node:path";
import { BASELINE_DIRECTORIES, BASELINE_FILES, TARGET_HOME_DIR } from "./constants";
import { antigravityWorkflowPath, workflowNameFromSkillName } from "./antigravity";
Expand All @@ -7,6 +8,7 @@ import { findSkillById, resolveInstallSelections } from "./catalogMultiSource";
import { copyPath, ensureDir, pathExists, removePath, trySymlinkDirectory, writeText } from "./fs";
import { mergeMcpConfig } from "./mcp";
import { computePlannerDelta } from "./planner";
import { getIcaGlobalRoot } from "./runtimePaths";
import { assertPathWithin, redactSensitive } from "./security";
import { appendHistory, createEmptyState, getStatePath, loadInstallState, reconcileLegacyManagedSkills, saveInstallState } from "./state";
import { computeDirectoryDigest } from "./contentDigest";
Expand Down Expand Up @@ -82,14 +84,101 @@ async function writeAntigravityWorkflow(
function buildBaselinePaths(resolved: ResolvedTargetPath): string[] {
const dirs = BASELINE_DIRECTORIES.map((dirName) => path.join(resolved.installPath, dirName));
const files = BASELINE_FILES.map((fileName) => path.join(resolved.installPath, fileName));
const paths = [...dirs, ...files, resolved.skillsPath, path.join(resolved.installPath, "logs"), path.join(resolved.installPath, "ica.config.json")];
const paths = [...dirs, ...files, resolved.skillsPath, path.join(resolved.installPath, "logs")];
if (isAntigravityTarget(resolved)) {
paths.push(resolved.workflowsPath);
}
return paths;
}

async function installBaseline(repoRoot: string, resolved: ResolvedTargetPath, configFile?: string): Promise<void> {
async function seedSharedGlobalConfig(
repoRoot: string,
resolved: ResolvedTargetPath,
report: TargetOperationReport,
configFile?: string,
): Promise<void> {
if (resolved.scope !== "user") {
return;
}

const globalRoot = getIcaGlobalRoot();
await ensureDir(globalRoot);

const legacyConfigCandidates = resolved.installPath !== globalRoot
? [path.join(resolved.installPath, "ica.config.json")]
: [];
const legacyWorkflowCandidates = resolved.installPath !== globalRoot
? [path.join(resolved.installPath, "ica.workflow.json")]
: [];

for (const targetHomeDir of Object.values(TARGET_HOME_DIR)) {
const candidateRoot = path.join(os.homedir(), targetHomeDir);
if (candidateRoot === resolved.installPath || candidateRoot === globalRoot) continue;
legacyConfigCandidates.push(path.join(candidateRoot, "ica.config.json"));
legacyWorkflowCandidates.push(path.join(candidateRoot, "ica.workflow.json"));
}

const globalConfigPath = path.join(globalRoot, "ica.config.json");
if (!(await pathExists(globalConfigPath))) {
const existingLegacyConfigs = [];
for (const candidate of legacyConfigCandidates) {
if (await pathExists(candidate)) {
existingLegacyConfigs.push(candidate);
}
}

if (configFile) {
await copyPath(path.resolve(configFile), globalConfigPath);
} else if (existingLegacyConfigs.length === 1) {
await copyPath(existingLegacyConfigs[0], globalConfigPath);
pushWarning(
report,
"GLOBAL_CONFIG_MIGRATED",
`Seeded shared ICA config from legacy agent-home override at '${existingLegacyConfigs[0]}'.`,
);
} else {
if (existingLegacyConfigs.length > 1) {
pushWarning(
report,
"AMBIGUOUS_LEGACY_GLOBAL_CONFIG",
`Found multiple legacy agent-home configs; seeded '${globalConfigPath}' from defaults instead of guessing.`,
);
}
await copyPath(path.join(repoRoot, "ica.config.default.json"), globalConfigPath);
}
}

const globalWorkflowPath = path.join(globalRoot, "ica.workflow.json");
if (!(await pathExists(globalWorkflowPath))) {
const existingLegacyWorkflows = [];
for (const candidate of legacyWorkflowCandidates) {
if (await pathExists(candidate)) {
existingLegacyWorkflows.push(candidate);
}
}

if (existingLegacyWorkflows.length === 1) {
await copyPath(existingLegacyWorkflows[0], globalWorkflowPath);
pushWarning(
report,
"GLOBAL_WORKFLOW_MIGRATED",
`Seeded shared ICA workflow from legacy agent-home override at '${existingLegacyWorkflows[0]}'.`,
);
} else {
if (existingLegacyWorkflows.length > 1) {
pushWarning(
report,
"AMBIGUOUS_LEGACY_GLOBAL_WORKFLOW",
`Found multiple legacy agent-home workflows; seeded '${globalWorkflowPath}' from defaults instead of guessing.`,
);
}
await copyPath(path.join(repoRoot, "ica.workflow.default.json"), globalWorkflowPath);
}
}
}

async function installBaseline(repoRoot: string, resolved: ResolvedTargetPath, report: TargetOperationReport, configFile?: string): Promise<void> {
await seedSharedGlobalConfig(repoRoot, resolved, report, configFile);
await ensureDir(resolved.installPath);
await ensureDir(resolved.skillsPath);
await ensureDir(path.join(resolved.installPath, "logs"));
Expand All @@ -112,13 +201,6 @@ async function installBaseline(repoRoot: string, resolved: ResolvedTargetPath, c

const defaultWorkflowSource = path.join(repoRoot, "ica.workflow.default.json");
await copyPath(defaultWorkflowSource, path.join(resolved.installPath, "ica.workflow.default.json"));

const targetConfig = path.join(resolved.installPath, "ica.config.json");
if (configFile) {
await copyPath(path.resolve(configFile), targetConfig);
} else if (!(await pathExists(targetConfig))) {
await copyPath(defaultConfigSource, targetConfig);
}
}

function pushWarning(report: TargetOperationReport, code: string, message: string): void {
Expand Down Expand Up @@ -498,7 +580,7 @@ async function installOrSyncTarget(
report: TargetOperationReport,
catalog: SkillCatalog,
): Promise<void> {
await installBaseline(repoRoot, resolved, request.configFile);
await installBaseline(repoRoot, resolved, report, request.configFile);

const rawState = (await loadInstallState(resolved.installPath)) ||
createEmptyState({
Expand Down
46 changes: 46 additions & 0 deletions src/installer-core/runtimePaths.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import os from "node:os";
import path from "node:path";
import { TARGET_HOME_DIR } from "./constants";
import { TargetPlatform } from "./types";

function cleanEnvPath(value: string | undefined): string | null {
if (!value || !value.trim()) {
return null;
}
return path.resolve(value.trim());
}

function normalizeTarget(value: string | undefined): TargetPlatform | null {
const normalized = (value || "").trim().toLowerCase();
if (
normalized === "claude" ||
normalized === "codex" ||
normalized === "cursor" ||
normalized === "gemini" ||
normalized === "antigravity"
) {
return normalized;
}
return null;
}

export function getIcaGlobalRoot(homeDir = os.homedir()): string {
return cleanEnvPath(process.env.ICA_STATE_HOME) ||
cleanEnvPath(process.env.ICA_GLOBAL_HOME) ||
path.resolve(homeDir, ".ica");
}

export function resolveActiveAgentHome(options: { homeDir?: string } = {}): string | null {
const explicitHome = cleanEnvPath(process.env.ICA_HOME);
if (explicitHome) {
return explicitHome;
}

const activeTarget = normalizeTarget(process.env.ICA_ACTIVE_TARGET);
if (!activeTarget) {
return null;
}

const homeDir = options.homeDir || os.homedir();
return path.resolve(homeDir, TARGET_HOME_DIR[activeTarget]);
}
21 changes: 12 additions & 9 deletions src/installer-core/trackingConfig.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import path from "node:path";
import { resolveActiveAgentHome } from "./runtimePaths";

export type TrackingProvider = "github" | "file-based" | "linear" | "jira";

Expand All @@ -16,6 +17,7 @@ export interface TrackingConfig {
export interface TrackingConfigDependencies {
cwd: string;
icaHome?: string;
activeAgentHome?: string;
homeDir: string;
pathExists: (targetPath: string) => Promise<boolean>;
readText: (targetPath: string) => Promise<string>;
Expand Down Expand Up @@ -81,21 +83,22 @@ function candidatePaths(deps: TrackingConfigDependencies): Array<{ path: string;
source: "project",
});

const activeAgentHome = deps.activeAgentHome && deps.activeAgentHome.trim().length > 0
? deps.activeAgentHome
: resolveActiveAgentHome({ homeDir: deps.homeDir });
if (activeAgentHome) {
candidates.push({
path: path.resolve(activeAgentHome, "tracking.config.json"),
source: "agent-home",
});
}

if (deps.icaHome && deps.icaHome.trim().length > 0) {
candidates.push({
path: path.resolve(deps.icaHome, "tracking.config.json"),
source: "system",
});
}

candidates.push({
path: path.resolve(deps.homeDir, ".codex", "tracking.config.json"),
source: "agent-home",
});
candidates.push({
path: path.resolve(deps.homeDir, ".claude", "tracking.config.json"),
source: "agent-home",
});
return candidates;
}

Expand Down
Loading
Loading