aiwg
Version:
Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo
271 lines • 12 kB
JavaScript
/**
* Shadow Resolver
*
* Implements the override / shadow-resolution policy from
* `.aiwg/architecture/adr-override-shadow-policy.md` (#1041) for project-local
* artifact bundles deployed via `aiwg use` / `aiwg refresh`.
*
* Resolves the seven cases from ADR §4:
*
* 1. No collision — deploy normally
* 2. Non-safety-critical shadow — deploy + warn
* 3. Safety-critical shadow with `overrides:` declaration — deploy + prominent warn
* 4. Safety-critical shadow without `overrides:` — REFUSE
* 5. Phantom override (declared override has no upstream match) — REFUSE
* 6. Two project-local bundles export the same id — REFUSE both
* 7. git-installed source collides with project-local — same as 2/3/4 against cache
*
* Inputs: the project-local bundles (from `discoverProjectLocalBundles`) and
* an upstream registry (from `buildUpstreamRegistry`). Outputs a per-artifact
* verdict that the deployment pipeline consumes to decide what to write to
* provider paths.
*
* @implements #1036
*/
import { readdir, readFile, stat } from 'fs/promises';
import { join, basename } from 'path';
const ARTIFACT_DIRS = {
agents: 'agent',
skills: 'skill',
rules: 'rule',
commands: 'command',
};
const FRONTMATTER_RE = /^---\s*\n([\s\S]*?)\n---/;
const ID_LINE_RE = /^id\s*:\s*['"]?([^'"\n]+?)['"]?\s*$/m;
const NAME_LINE_RE = /^name\s*:\s*['"]?([^'"\n]+?)['"]?\s*$/m;
function parseId(raw) {
const m = FRONTMATTER_RE.exec(raw);
if (!m)
return {};
return {
id: ID_LINE_RE.exec(m[1])?.[1]?.trim(),
name: NAME_LINE_RE.exec(m[1])?.[1]?.trim(),
};
}
/** Enumerate the artifacts a project-local bundle would deploy by walking its
* source `agents/`, `skills/`, `rules/`, `commands/` subdirs — same pattern as
* `deployOneProjectLocalBundle` and `countBundleSourceArtifacts` in use.ts. */
export async function enumerateBundleArtifacts(bundlePath) {
const out = [];
for (const [dirName, type] of Object.entries(ARTIFACT_DIRS)) {
const artifactDir = join(bundlePath, dirName);
let entries;
try {
entries = await readdir(artifactDir);
}
catch {
continue;
}
if (type === 'skill') {
for (const entry of entries) {
const skillDir = join(artifactDir, entry);
try {
const st = await stat(skillDir);
if (!st.isDirectory())
continue;
}
catch {
continue;
}
let id = entry;
try {
const skillMd = await readFile(join(skillDir, 'SKILL.md'), 'utf-8');
const fm = parseId(skillMd);
id = fm.name ?? fm.id ?? entry;
}
catch {
// No SKILL.md — fall back to dir name
}
out.push({ id, type, sourcePath: skillDir });
}
continue;
}
for (const entry of entries) {
if (!entry.endsWith('.md'))
continue;
if (entry === 'README.md' || entry === 'RULES-INDEX.md' || entry === 'INDEX.md')
continue;
const filePath = join(artifactDir, entry);
let id = basename(entry, '.md');
try {
const raw = await readFile(filePath, 'utf-8');
const fm = parseId(raw);
id = fm.id ?? fm.name ?? id;
}
catch {
// Read failure — fall back to filename
}
out.push({ id, type, sourcePath: filePath });
}
}
return out;
}
/** Resolve overrides + shadows for a set of project-local bundles against an
* upstream registry. Pure function — no filesystem side effects beyond reading
* the bundle artifact files for id extraction. */
export async function resolveShadows(bundles, upstream, options = {}) {
const strictPhantomOverrides = options.strictPhantomOverrides ?? true;
const resolutions = [];
const blockedBundleIds = new Set();
// Pre-pass: case 6 — duplicate project-local bundle ids (cross-bundle, by
// artifact id+type). Discovery already rejects same-type duplicate bundle
// ids (#1041 §4 case 6 at the bundle level); here we additionally guard
// against two distinct bundles exporting an artifact with the same id+type.
const projectLocalArtifacts = new Map();
// Enumerate every artifact in every bundle once.
const enumerated = [];
for (const bundle of bundles) {
const arts = await enumerateBundleArtifacts(bundle.bundlePath);
enumerated.push({ bundle, arts });
for (const art of arts) {
const key = `${art.type}:${art.id}`;
const list = projectLocalArtifacts.get(key) ?? [];
list.push({ bundle, art });
projectLocalArtifacts.set(key, list);
}
}
for (const { bundle, arts } of enumerated) {
const declaredOverrides = new Set(bundle.manifest.overrides ?? []);
// Case 5 pre-pass: validate every declared override resolves to *some*
// upstream artifact (any type). Phantom overrides refuse the bundle.
for (const overrideId of declaredOverrides) {
if (!upstream.byId.has(overrideId)) {
resolutions.push({
bundleId: bundle.id,
bundleLocalPath: bundle.localPath,
artifactId: overrideId,
artifactType: 'rule', // unknown — picked nominal type for surface
artifactSourcePath: bundle.manifestPath,
verdict: 'refuse-phantom',
message: `Phantom override: '${overrideId}' declared in ${bundle.manifestPath} but no upstream artifact has that id.`,
blocking: strictPhantomOverrides,
prominent: false,
});
if (strictPhantomOverrides)
blockedBundleIds.add(bundle.id);
}
}
for (const art of arts) {
const key = `${art.type}:${art.id}`;
// Case 6 — duplicate project-local artifact id+type across bundles
const projectLocalGroup = projectLocalArtifacts.get(key) ?? [];
if (projectLocalGroup.length > 1) {
const others = projectLocalGroup
.filter((p) => p.bundle.id !== bundle.id)
.map((p) => p.bundle.localPath);
resolutions.push({
bundleId: bundle.id,
bundleLocalPath: bundle.localPath,
artifactId: art.id,
artifactType: art.type,
artifactSourcePath: art.sourcePath,
verdict: 'refuse-duplicate',
message: `Duplicate project-local ${art.type} '${art.id}' also exported by: ${others.join(', ')}`,
blocking: true,
prominent: false,
});
blockedBundleIds.add(bundle.id);
continue;
}
const upstreamMatch = upstream.byKey.get(key);
// Case 1 — no collision
if (!upstreamMatch) {
resolutions.push({
bundleId: bundle.id,
bundleLocalPath: bundle.localPath,
artifactId: art.id,
artifactType: art.type,
artifactSourcePath: art.sourcePath,
verdict: 'deploy',
message: '',
blocking: false,
prominent: false,
});
continue;
}
const acknowledged = declaredOverrides.has(art.id);
if (upstreamMatch.safetyCritical) {
if (acknowledged) {
// Case 3 — safety-critical with explicit override
resolutions.push({
bundleId: bundle.id,
bundleLocalPath: bundle.localPath,
artifactId: art.id,
artifactType: art.type,
artifactSourcePath: art.sourcePath,
upstream: upstreamMatch,
verdict: 'deploy-acknowledged',
message: `SAFETY-CRITICAL SHADOW: ${art.type} '${art.id}' overridden by ${art.sourcePath}.\n` +
` Acknowledge: this disables upstream safeguard at ${upstreamMatch.sourcePath}.\n` +
` Use 'aiwg doctor' to review all active shadows.`,
blocking: false,
prominent: true,
});
}
else {
// Case 4 — refuse
resolutions.push({
bundleId: bundle.id,
bundleLocalPath: bundle.localPath,
artifactId: art.id,
artifactType: art.type,
artifactSourcePath: art.sourcePath,
upstream: upstreamMatch,
verdict: 'refuse-unsafe',
message: `Refused to shadow safety-critical upstream ${art.type} '${art.id}'. ` +
`Add 'overrides: ["${art.id}"]' to ${bundle.manifestPath} to authorize the override.`,
blocking: true,
prominent: true,
});
blockedBundleIds.add(bundle.id);
}
continue;
}
// Case 2 — non-safety shadow
const sourceLabel = upstreamMatch.source === 'cache' ? 'git-installed' : 'bundled';
resolutions.push({
bundleId: bundle.id,
bundleLocalPath: bundle.localPath,
artifactId: art.id,
artifactType: art.type,
artifactSourcePath: art.sourcePath,
upstream: upstreamMatch,
verdict: 'deploy-with-warning',
message: `Shadow: ${art.type} '${art.id}' — project-local at ${art.sourcePath} ` +
`overrides ${sourceLabel} at ${upstreamMatch.sourcePath}`,
blocking: false,
prominent: false,
});
}
}
const shadows = resolutions.filter((r) => r.upstream !== undefined);
return { resolutions, blockedBundleIds, shadows };
}
/** Format a multi-resolution summary suitable for stderr or doctor output. */
export function formatShadowReport(result) {
if (result.resolutions.length === 0)
return '';
const lines = [];
const blockers = result.resolutions.filter((r) => r.blocking);
const warnings = result.resolutions.filter((r) => !r.blocking && (r.verdict === 'deploy-with-warning' || r.verdict === 'deploy-acknowledged'));
if (blockers.length > 0) {
lines.push('── Project-local shadow resolution: blocked artifacts ──');
for (const r of blockers) {
lines.push(` ✗ [${r.verdict}] ${r.bundleId} :: ${r.artifactType}/${r.artifactId}`);
for (const ml of r.message.split('\n'))
lines.push(` ${ml}`);
}
lines.push('');
}
if (warnings.length > 0) {
lines.push('── Project-local shadows ──');
for (const r of warnings) {
const marker = r.prominent ? '!!' : '⚠';
lines.push(` ${marker} [${r.verdict}] ${r.bundleId} :: ${r.artifactType}/${r.artifactId}`);
for (const ml of r.message.split('\n'))
lines.push(` ${ml}`);
}
}
return lines.join('\n');
}
//# sourceMappingURL=shadow-resolver.js.map