Files
fusion/plugins/fusion-plugin-compound-engineering/src/agent-installation.ts
gsxdsm aa401123ea fix: make Windows updates and CE personas reliable (#2340)
## Summary

Windows installs with slow native dependencies now get five minutes to
finish, and a real timeout is reported as an actionable terminal retry
instead of a wall of preceding npm deprecation warnings. Registry
`ETIMEDOUT` errors keep their network diagnosis, including after the
legacy-bin `--force` retry.

Compound Engineering personas are now included in the published CLI
bundle, with complete source-to-staged coverage for all persona
definitions and a clear startup error if the bundled assets are missing
or empty.

The PostgreSQL statement visible in the report was validated by the
existing real-Postgres schema reapply test. Its actual `caused by`
detail was truncated, so this PR deliberately makes no speculative
database change.

## Validation

- Dashboard updater tests: 22 passed
- CLI updater tests: 16 passed
- CE persona installer tests: 7 passed
- Published bundle persona assertion: passed against every source
persona
- CLI and CE plugin typechecks: passed
- Changed production/config lint and strict changeset validation: passed
- Real PostgreSQL schema reapply integration test: passed

---

[![Compound
Engineering](https://img.shields.io/badge/Built_with-Compound_Engineering-6366f1)](https://github.com/EveryInc/compound-engineering-plugin)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Windows CLI and dashboard updates now allow up to five minutes for
installation and restore Compound Engineering agent personas during npm
installs.
* Update failures now surface clearer, terminal timeout guidance (while
preserving specific network connection diagnostics) and avoid misleading
“deprecated”/generic timeout text.
* Persona assets are reliably included in plugin builds and bunded
persona installation now errors clearly when definitions are missing or
empty.
* **Tests**
* Expanded update and bundling coverage for the new 5-minute timeout and
error-handling scenarios.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-19 12:16:12 -07:00

204 lines
7.7 KiB
TypeScript

import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { dirname, isAbsolute, join, resolve, sep } from "node:path";
import { fileURLToPath } from "node:url";
import { CE_UPSTREAM_PROVENANCE } from "./upstream-provenance.js";
/**
* Physical install of the bundled Compound Engineering agent persona
* definitions (the `ce-*` reviewer/research personas the CE skills fan out to).
*
* WHY A PHYSICAL INSTALL + ENV, NOT A PLUGIN CONTRIBUTION (spike finding):
* Fusion has no plugin agent-contribution channel — `FusionPlugin` contributes
* skills/workflowSteps/traits but not agents, and `fn_spawn_agent` resolves no
* persona by name. So the CE skills running inside a workflow step read a
* persona def from disk and pass its body to `fn_spawn_agent` via the
* `systemPromptOverride` param. For the skill to find the defs, they are
* installed into a plugin-local directory whose path is exported to step
* sessions through the plugin's `executorRuntimeEnv` hook (FUSION_CE_AGENTS_DIR).
*
* Mirrors `skill-installation.ts`: cpSync into a plugin-local directory, never a
* global `<home>/.claude/agents` path. Existing plugin-local copies are refreshed
* when their provenance marker is absent or stale so prompt persona updates reach
* existing enabled plugins.
*/
export type CeAgentInstallOutcome = "installed" | "refreshed" | "skipped" | "error";
export interface CeAgentInstallResult {
agentId: string;
sourceFile: string;
targetFile: string;
outcome: CeAgentInstallOutcome;
reason?: string;
}
export interface InstallBundledCeAgentsResult {
targetRoot: string;
results: CeAgentInstallResult[];
}
/** Absolute path to the plugin's bundled `src/agents` directory (source of truth). */
export function resolveBundledAgentsRoot(): string {
const here = fileURLToPath(import.meta.url);
const dir = dirname(here);
const local = resolve(dir, "agents");
if (existsSync(local)) return local;
return resolve(dir, "..", "src", "agents");
}
/**
* Default plugin-local install target (`.fusion-ce-agents/`). ALWAYS plugin-local
* — never a global client agents directory.
*/
export function resolveDefaultAgentsInstallTargetRoot(): string {
const here = fileURLToPath(import.meta.url);
return resolve(dirname(here), "..", ".fusion-ce-agents");
}
const GLOBAL_AGENT_DIR_PATTERN = /[\\/]\.(claude|codex|gemini)[\\/]agents([\\/]|$)/;
/** Guard: refuse to install into a global client agents directory. */
export function assertPluginLocalAgentsTarget(targetRoot: string): void {
const normalized = resolve(targetRoot);
if (GLOBAL_AGENT_DIR_PATTERN.test(normalized + sep)) {
throw new Error(
`Refusing to install Compound Engineering agents into a global client agents directory: ${normalized}. ` +
`Install target MUST be plugin-local (never <home>/.claude|.codex|.gemini/agents).`,
);
}
}
/** A bundled agent def must exist and carry a frontmatter `name:`. */
function assertValidAgentSource(agentId: string, sourceFile: string): void {
if (!existsSync(sourceFile)) {
throw new Error(`Bundled agent def missing for '${agentId}': ${sourceFile}`);
}
const content = readFileSync(sourceFile, "utf-8");
if (!/^---[\s\S]*?\bname\s*:\s*\S/m.test(content)) {
throw new Error(`Bundled agent def '${agentId}' at ${sourceFile} is missing a frontmatter 'name:' field`);
}
}
export interface InstallBundledCeAgentsOptions {
/** Override the install target root (must be plugin-local). */
targetRoot?: string;
/** Override the bundled source root (tests). */
sourceRoot?: string;
}
const AGENT_INSTALL_PROVENANCE_FILE = ".fusion-ce-upstream-provenance.json";
function installProvenancePath(targetRoot: string): string {
return join(targetRoot, AGENT_INSTALL_PROVENANCE_FILE);
}
function isCurrentInstalledProvenance(targetRoot: string): boolean {
try {
const marker = JSON.parse(readFileSync(installProvenancePath(targetRoot), "utf-8")) as Partial<
typeof CE_UPSTREAM_PROVENANCE
>;
return (
marker.releaseTag === CE_UPSTREAM_PROVENANCE.releaseTag &&
marker.tarballSha256 === CE_UPSTREAM_PROVENANCE.tarballSha256
);
} catch {
return false;
}
}
function writeInstalledProvenance(targetRoot: string): void {
mkdirSync(targetRoot, { recursive: true });
writeFileSync(
installProvenancePath(targetRoot),
`${JSON.stringify(
{
repo: CE_UPSTREAM_PROVENANCE.repo,
releaseTag: CE_UPSTREAM_PROVENANCE.releaseTag,
commit: CE_UPSTREAM_PROVENANCE.commit,
tarballSha256: CE_UPSTREAM_PROVENANCE.tarballSha256,
installedAt: new Date().toISOString(),
},
null,
2,
)}\n`,
);
}
/**
* Copy each bundled `ce-*.md` agent def into the plugin-local install target.
*
* FNXC:CompoundEngineering 2026-06-26-23:55:
* Persona prompts are part of the pinned upstream bundle, so the plugin-local agent install uses the same provenance-aware refresh policy as skills. This prevents stale skip-if-exists installs from continuing to dispatch old personas after a vendored refresh.
*/
export function installBundledCeAgents(
options: InstallBundledCeAgentsOptions = {},
): InstallBundledCeAgentsResult {
const targetRoot = options.targetRoot
? resolve(options.targetRoot)
: resolveDefaultAgentsInstallTargetRoot();
assertPluginLocalAgentsTarget(targetRoot);
const sourceRoot = options.sourceRoot ? resolve(options.sourceRoot) : resolveBundledAgentsRoot();
const installIsCurrent = isCurrentInstalledProvenance(targetRoot);
let sourceFiles: string[];
/*
FNXC:CompoundEngineeringAgents 2026-07-19-09:50:
A published package without bundled persona definitions is corrupt. Fail at plugin startup instead of silently installing zero agents and breaking later skill fanout.
*/
try {
sourceFiles = readdirSync(sourceRoot).filter((file) => file.endsWith(".md"));
} catch (error) {
const code = (error as NodeJS.ErrnoException).code;
if (code === "ENOENT" || code === "ENOTDIR") {
throw new Error(`Bundled agent persona source directory is missing: ${sourceRoot}`, { cause: error });
}
throw error;
}
if (sourceFiles.length === 0) {
throw new Error(`Bundled agent persona source directory contains no markdown definitions: ${sourceRoot}`);
}
const results = sourceFiles.map<CeAgentInstallResult>((file) => {
const agentId = file.replace(/\.md$/, "");
const sourceFile = join(sourceRoot, file);
const targetFile = join(targetRoot, file);
try {
assertValidAgentSource(agentId, sourceFile);
if (existsSync(targetFile)) {
if (installIsCurrent) {
return { agentId, sourceFile, targetFile, outcome: "skipped", reason: "current install preserved" };
}
rmSync(targetFile, { force: true });
mkdirSync(targetRoot, { recursive: true });
cpSync(sourceFile, targetFile);
return { agentId, sourceFile, targetFile, outcome: "refreshed", reason: "stale install refreshed" };
}
mkdirSync(targetRoot, { recursive: true });
cpSync(sourceFile, targetFile);
return { agentId, sourceFile, targetFile, outcome: "installed" };
} catch (error) {
return {
agentId,
sourceFile,
targetFile,
outcome: "error",
reason: error instanceof Error ? error.message : String(error),
};
}
});
if (results.length > 0 && results.every((result) => result.outcome !== "error")) {
writeInstalledProvenance(targetRoot);
}
return { targetRoot, results };
}
/** True if the given path is absolute and not inside a global client agents dir. */
export function isPluginLocalAgentsPath(p: string): boolean {
return isAbsolute(p) && !GLOBAL_AGENT_DIR_PATTERN.test(resolve(p) + sep);
}