mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-09 11:51:37 -10:00
feat(daemon): add resources.plugins[] + profile.uses.plugins[]; remove resources.hooks[]
types.ts: - Add PluginResource interface + PluginSource union (kind: local at v0) - Add AgentResources.plugins[]; remove AgentResources.hooks[] - Add ProfileSpec.uses.plugins[]; remove ProfileSpec.uses.hooks[] - Remove HookResource interface agent-manifest.ts: - Validator iterates plugins category in place of hooks - normalizeResources returns plugins[]; drops hooks[] - New validatePluginResources + normalizePluginResource (different shape: id + source object, not id + path) - Plugin paths NOT subject to in-spec safe-path check (operator-managed absolute paths permitted: ~/.openrig/plugins/<id>/, etc.) profile-resolver.ts: - RESOURCE_CATEGORIES + YAML_CATEGORY_MAP swap hooks → plugins - ResolvedResources type swap; resolveProfileUses + buildResourcePool carry plugin entries through pool projection-planner.ts: - ProjectionEntry.category swap hook → plugin - CATEGORY_MAP swap - Plugin source.path extraction in entry-build loop (different shape from skill/guidance entries) - checkAmbiguity covers plugins category agent-resolver.ts: - RESOURCE_CATEGORIES swap hooks → plugins claude-code-adapter.ts + codex-runtime-adapter.ts: - resolveTargetDir swap case 'hook' → case 'plugin' - Claude target: <cwd>/.claude/plugins/<id>/ - Codex target: <cwd>/.codex/plugins/<id>/ Adapter projectEntry hook-injection methods (provisionActivityHooks, upsertCommandHook, hookEntryContainsCommand) NOT YET removed in this commit — that's the next pass after tests confirm baseline rip is sound. Tests still need updating in next commit; daemon tsc clean at this checkpoint. Per IMPL-PRD §1 plugin-primitive Phase 3a slice 3.1.
This commit is contained in:
1 parent
c0fcccd102
commit
88ee79cac1
7 files changed
+112
-32
No files matched your search
@@ -384,7 +384,7 @@ export class ClaudeCodeAdapter implements RuntimeAdapter {
|
||||
case "skill": return nodePath.join(cwd, ".claude", "skills", entry.effectiveId);
|
||||
case "guidance": return null; // handled via merge
|
||||
case "subagent": return nodePath.join(cwd, ".claude", "agents");
|
||||
case "hook": return nodePath.join(cwd, ".claude", "hooks");
|
||||
case "plugin": return nodePath.join(cwd, ".claude", "plugins", entry.effectiveId);
|
||||
case "runtime_resource": return nodePath.join(cwd, ".claude", "extensions", entry.effectiveId);
|
||||
default: return null;
|
||||
}
|
||||
|
||||
@@ -345,7 +345,7 @@ export class CodexRuntimeAdapter implements RuntimeAdapter {
|
||||
case "skill": return nodePath.join(cwd, ".agents", "skills", entry.effectiveId);
|
||||
case "guidance": return null; // handled via merge
|
||||
case "subagent": return nodePath.join(cwd, ".agents"); // .agents/{id}.yaml per preserved contract
|
||||
case "hook": return nodePath.join(cwd, ".agents", "hooks");
|
||||
case "plugin": return nodePath.join(cwd, ".codex", "plugins", entry.effectiveId);
|
||||
case "runtime_resource": return nodePath.join(cwd, ".agents", "extensions", entry.effectiveId);
|
||||
default: return null;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@ import { parse as parseYaml } from "yaml";
|
||||
import type {
|
||||
AgentSpec, ImportSpec, StartupBlock, StartupFile, StartupAction,
|
||||
LifecycleDefaults, AgentResources, ProfileSpec,
|
||||
SkillResource, GuidanceResource, SubagentResource, HookResource, RuntimeResource,
|
||||
SkillResource, GuidanceResource, SubagentResource, RuntimeResource,
|
||||
PluginResource, PluginSource,
|
||||
ValidationResult,
|
||||
} from "./types.js";
|
||||
import {
|
||||
@@ -173,15 +174,22 @@ export function validateAgentSpec(raw: unknown): ValidationResult {
|
||||
const res = obj["resources"] as Record<string, unknown>;
|
||||
const allLocalIds: Record<string, Set<string>> = {};
|
||||
|
||||
for (const category of ["skills", "guidance", "subagents", "hooks", "runtime_resources"]) {
|
||||
for (const category of ["skills", "guidance", "subagents", "plugins", "runtime_resources"]) {
|
||||
const items = res[category];
|
||||
if (items !== undefined) {
|
||||
if (!Array.isArray(items)) {
|
||||
errors.push(`resources.${category}: must be an array`);
|
||||
} else {
|
||||
const entries = items as Array<Record<string, unknown>>;
|
||||
errors.push(...validateResourcePaths(entries as Array<{ id: string; path: string }>, category));
|
||||
allLocalIds[category] = new Set(entries.map((e) => e["id"] as string).filter(Boolean));
|
||||
|
||||
if (category === "plugins") {
|
||||
// Plugin entries have a different shape (id + source object) — validate inline
|
||||
errors.push(...validatePluginResources(entries));
|
||||
allLocalIds[category] = new Set(entries.map((e) => e["id"] as string).filter(Boolean));
|
||||
} else {
|
||||
errors.push(...validateResourcePaths(entries as Array<{ id: string; path: string }>, category));
|
||||
allLocalIds[category] = new Set(entries.map((e) => e["id"] as string).filter(Boolean));
|
||||
}
|
||||
|
||||
// runtime_resources must have runtime field
|
||||
if (category === "runtime_resources") {
|
||||
@@ -207,7 +215,7 @@ export function validateAgentSpec(raw: unknown): ValidationResult {
|
||||
const p = profileRaw as Record<string, unknown>;
|
||||
if (p["uses"] && typeof p["uses"] === "object") {
|
||||
const uses = p["uses"] as Record<string, unknown>;
|
||||
for (const category of ["skills", "guidance", "subagents", "hooks", "runtime_resources"]) {
|
||||
for (const category of ["skills", "guidance", "subagents", "plugins", "runtime_resources"]) {
|
||||
const refs = uses[category];
|
||||
if (Array.isArray(refs)) {
|
||||
for (const ref of refs as string[]) {
|
||||
@@ -300,7 +308,7 @@ function normalizeLifecycle(raw: Record<string, unknown>): LifecycleDefaults {
|
||||
|
||||
function normalizeResources(raw: unknown): AgentResources {
|
||||
if (!raw || typeof raw !== "object") {
|
||||
return { skills: [], guidance: [], subagents: [], hooks: [], runtimeResources: [] };
|
||||
return { skills: [], guidance: [], subagents: [], plugins: [], runtimeResources: [] };
|
||||
}
|
||||
const obj = raw as Record<string, unknown>;
|
||||
|
||||
@@ -317,11 +325,8 @@ function normalizeResources(raw: unknown): AgentResources {
|
||||
subagents: Array.isArray(obj["subagents"])
|
||||
? (obj["subagents"] as Record<string, unknown>[]).map((s) => ({ id: s["id"] as string, path: s["path"] as string }))
|
||||
: [],
|
||||
hooks: Array.isArray(obj["hooks"])
|
||||
? (obj["hooks"] as Record<string, unknown>[]).map((h) => ({
|
||||
id: h["id"] as string, path: h["path"] as string,
|
||||
runtimes: Array.isArray(h["runtimes"]) ? h["runtimes"] as string[] : undefined,
|
||||
}))
|
||||
plugins: Array.isArray(obj["plugins"])
|
||||
? (obj["plugins"] as Record<string, unknown>[]).map(normalizePluginResource)
|
||||
: [],
|
||||
runtimeResources: Array.isArray(obj["runtime_resources"])
|
||||
? (obj["runtime_resources"] as Record<string, unknown>[]).map((r) => ({
|
||||
@@ -332,6 +337,63 @@ function normalizeResources(raw: unknown): AgentResources {
|
||||
};
|
||||
}
|
||||
|
||||
function normalizePluginResource(raw: Record<string, unknown>): PluginResource {
|
||||
const sourceRaw = (raw["source"] ?? {}) as Record<string, unknown>;
|
||||
const source: PluginSource = {
|
||||
kind: "local",
|
||||
path: sourceRaw["path"] as string,
|
||||
};
|
||||
const result: PluginResource = {
|
||||
id: raw["id"] as string,
|
||||
source,
|
||||
};
|
||||
const pluginType = raw["plugin_type"] ?? raw["pluginType"];
|
||||
if (pluginType === "claude" || pluginType === "codex" || pluginType === "auto") {
|
||||
result.pluginType = pluginType;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function validatePluginResources(entries: Array<Record<string, unknown>>): string[] {
|
||||
const errors: string[] = [];
|
||||
const ids = new Set<string>();
|
||||
for (let i = 0; i < entries.length; i++) {
|
||||
const entry = entries[i]!;
|
||||
if (!entry["id"] || typeof entry["id"] !== "string") {
|
||||
errors.push(`resources.plugins[${i}].id: must be a non-empty string`);
|
||||
} else if (ids.has(entry["id"] as string)) {
|
||||
errors.push(`resources.plugins: duplicate id "${entry["id"]}"`);
|
||||
} else {
|
||||
ids.add(entry["id"] as string);
|
||||
}
|
||||
|
||||
const source = entry["source"];
|
||||
if (!source || typeof source !== "object" || Array.isArray(source)) {
|
||||
errors.push(`resources.plugins[${i}].source: must be an object with kind + source-specific fields`);
|
||||
continue;
|
||||
}
|
||||
const sourceObj = source as Record<string, unknown>;
|
||||
const kind = sourceObj["kind"];
|
||||
if (kind !== "local") {
|
||||
errors.push(`resources.plugins[${i}].source.kind: only "local" is supported in v0 (got "${String(kind)}")`);
|
||||
continue;
|
||||
}
|
||||
// Plugin paths may be absolute (e.g. ~/.openrig/plugins/<id> for vendored
|
||||
// plugins, or absolute project paths). Skip the safe-path check used for
|
||||
// in-spec resources; plugin sources are explicit operator-managed paths.
|
||||
const pluginPath = sourceObj["path"];
|
||||
if (!pluginPath || typeof pluginPath !== "string") {
|
||||
errors.push(`resources.plugins[${i}].source.path: must be a non-empty string`);
|
||||
}
|
||||
|
||||
const pluginType = entry["plugin_type"] ?? entry["pluginType"];
|
||||
if (pluginType !== undefined && pluginType !== "claude" && pluginType !== "codex" && pluginType !== "auto") {
|
||||
errors.push(`resources.plugins[${i}].plugin_type: must be "claude" | "codex" | "auto" (got "${String(pluginType)}")`);
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
function normalizeProfile(raw: Record<string, unknown>): ProfileSpec {
|
||||
const uses = raw["uses"] as Record<string, unknown> | undefined;
|
||||
return {
|
||||
@@ -343,7 +405,7 @@ function normalizeProfile(raw: Record<string, unknown>): ProfileSpec {
|
||||
skills: Array.isArray(uses?.["skills"]) ? uses["skills"] as string[] : [],
|
||||
guidance: Array.isArray(uses?.["guidance"]) ? uses["guidance"] as string[] : [],
|
||||
subagents: Array.isArray(uses?.["subagents"]) ? uses["subagents"] as string[] : [],
|
||||
hooks: Array.isArray(uses?.["hooks"]) ? uses["hooks"] as string[] : [],
|
||||
plugins: Array.isArray(uses?.["plugins"]) ? uses["plugins"] as string[] : [],
|
||||
runtimeResources: Array.isArray(uses?.["runtime_resources"]) ? uses["runtime_resources"] as string[] : [],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -34,7 +34,7 @@ export type ResolveResult =
|
||||
// -- Resource category keys --
|
||||
|
||||
const RESOURCE_CATEGORIES: (keyof AgentResources)[] = [
|
||||
"skills", "guidance", "subagents", "hooks", "runtimeResources",
|
||||
"skills", "guidance", "subagents", "plugins", "runtimeResources",
|
||||
];
|
||||
|
||||
// -- Public API --
|
||||
|
||||
@@ -2,7 +2,7 @@ import nodePath from "node:path";
|
||||
import type {
|
||||
AgentSpec, AgentResources, ProfileSpec, LifecycleDefaults,
|
||||
RigSpec, RigSpecPod, RigSpecPodMember, StartupBlock,
|
||||
SkillResource, GuidanceResource, SubagentResource, HookResource, RuntimeResource,
|
||||
SkillResource, GuidanceResource, SubagentResource, RuntimeResource, PluginResource,
|
||||
} from "./types.js";
|
||||
import type { ResolvedAgentSpec, ResourceCollision } from "./agent-resolver.js";
|
||||
import { resolveStartup } from "./startup-resolver.js";
|
||||
@@ -13,14 +13,14 @@ export interface QualifiedResource {
|
||||
effectiveId: string;
|
||||
sourceSpec: string;
|
||||
sourcePath: string;
|
||||
resource: SkillResource | GuidanceResource | SubagentResource | HookResource | RuntimeResource;
|
||||
resource: SkillResource | GuidanceResource | SubagentResource | RuntimeResource | PluginResource;
|
||||
}
|
||||
|
||||
export interface ResolvedResources {
|
||||
skills: QualifiedResource[];
|
||||
guidance: QualifiedResource[];
|
||||
subagents: QualifiedResource[];
|
||||
hooks: QualifiedResource[];
|
||||
plugins: QualifiedResource[];
|
||||
runtimeResources: QualifiedResource[];
|
||||
}
|
||||
|
||||
@@ -56,14 +56,14 @@ export type ResolutionResult =
|
||||
|
||||
// -- Constants --
|
||||
|
||||
const RESOURCE_CATEGORIES = ["skills", "guidance", "subagents", "hooks", "runtimeResources"] as const;
|
||||
const RESOURCE_CATEGORIES = ["skills", "guidance", "subagents", "plugins", "runtimeResources"] as const;
|
||||
type ResourceCategory = typeof RESOURCE_CATEGORIES[number];
|
||||
|
||||
const YAML_CATEGORY_MAP: Record<string, ResourceCategory> = {
|
||||
skills: "skills",
|
||||
guidance: "guidance",
|
||||
subagents: "subagents",
|
||||
hooks: "hooks",
|
||||
plugins: "plugins",
|
||||
runtime_resources: "runtimeResources",
|
||||
runtimeResources: "runtimeResources",
|
||||
};
|
||||
@@ -162,7 +162,7 @@ interface PoolEntry {
|
||||
effectiveId: string;
|
||||
sourceSpec: string;
|
||||
sourcePath: string;
|
||||
resource: SkillResource | GuidanceResource | SubagentResource | HookResource | RuntimeResource;
|
||||
resource: SkillResource | GuidanceResource | SubagentResource | RuntimeResource | PluginResource;
|
||||
}
|
||||
|
||||
type ResourcePool = Record<ResourceCategory, Map<string, PoolEntry[]>>;
|
||||
@@ -172,7 +172,7 @@ function buildResourcePool(base: ResolvedAgentSpec, imports: ResolvedAgentSpec[]
|
||||
skills: new Map(),
|
||||
guidance: new Map(),
|
||||
subagents: new Map(),
|
||||
hooks: new Map(),
|
||||
plugins: new Map(),
|
||||
runtimeResources: new Map(),
|
||||
};
|
||||
|
||||
@@ -231,7 +231,7 @@ function resolveProfileUses(
|
||||
skills: [],
|
||||
guidance: [],
|
||||
subagents: [],
|
||||
hooks: [],
|
||||
plugins: [],
|
||||
runtimeResources: [],
|
||||
};
|
||||
|
||||
@@ -239,7 +239,7 @@ function resolveProfileUses(
|
||||
skills: profile.uses.skills,
|
||||
guidance: profile.uses.guidance,
|
||||
subagents: profile.uses.subagents,
|
||||
hooks: profile.uses.hooks,
|
||||
plugins: profile.uses.plugins,
|
||||
runtimeResources: profile.uses.runtimeResources,
|
||||
};
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import type { ResourceCollision } from "./agent-resolver.js";
|
||||
export type ProjectionClassification = "safe_projection" | "managed_merge" | "hash_conflict" | "no_op";
|
||||
|
||||
export interface ProjectionEntry {
|
||||
category: "skill" | "guidance" | "subagent" | "hook" | "runtime_resource";
|
||||
category: "skill" | "guidance" | "subagent" | "plugin" | "runtime_resource";
|
||||
effectiveId: string;
|
||||
sourceSpec: string;
|
||||
sourcePath: string;
|
||||
@@ -55,7 +55,7 @@ const CATEGORY_MAP: Record<string, ProjectionEntry["category"]> = {
|
||||
skills: "skill",
|
||||
guidance: "guidance",
|
||||
subagents: "subagent",
|
||||
hooks: "hook",
|
||||
plugins: "plugin",
|
||||
runtimeResources: "runtime_resource",
|
||||
};
|
||||
|
||||
@@ -95,8 +95,18 @@ export function planProjection(input: ProjectionInput): PlanResult {
|
||||
if (rr.runtime !== config.runtime) continue;
|
||||
}
|
||||
|
||||
const resourcePath = (qr.resource as { path: string }).path;
|
||||
const absolutePath = nodePath.resolve(qr.sourcePath, resourcePath);
|
||||
// Plugins use a different shape: { id, source: { kind, path } } — extract path from source
|
||||
let resourcePath: string;
|
||||
let absolutePath: string;
|
||||
if (catKey === "plugins") {
|
||||
const pluginSource = (qr.resource as { source: { kind: string; path: string } }).source;
|
||||
resourcePath = pluginSource.path;
|
||||
// Plugin paths may be absolute (vendored at ~/.openrig/plugins/) or relative to spec root
|
||||
absolutePath = nodePath.isAbsolute(resourcePath) ? resourcePath : nodePath.resolve(qr.sourcePath, resourcePath);
|
||||
} else {
|
||||
resourcePath = (qr.resource as { path: string }).path;
|
||||
absolutePath = nodePath.resolve(qr.sourcePath, resourcePath);
|
||||
}
|
||||
|
||||
const entry: ProjectionEntry = {
|
||||
category: catSingular,
|
||||
@@ -176,7 +186,7 @@ function checkAmbiguity(selected: ResolvedResources, collisions: ResourceCollisi
|
||||
{ category: "skills", resources: selected.skills },
|
||||
{ category: "guidance", resources: selected.guidance },
|
||||
{ category: "subagents", resources: selected.subagents },
|
||||
{ category: "hooks", resources: selected.hooks },
|
||||
{ category: "plugins", resources: selected.plugins },
|
||||
{ category: "runtimeResources", resources: selected.runtimeResources },
|
||||
];
|
||||
|
||||
|
||||
@@ -511,14 +511,22 @@ export interface LifecycleDefaults {
|
||||
export interface SkillResource { id: string; path: string; }
|
||||
export interface GuidanceResource { id: string; path: string; target: string; merge: "managed_block" | "append"; }
|
||||
export interface SubagentResource { id: string; path: string; }
|
||||
export interface HookResource { id: string; path: string; runtimes?: string[]; }
|
||||
export interface RuntimeResource { id: string; path: string; runtime: string; type: string; }
|
||||
|
||||
export type PluginSource =
|
||||
| { kind: "local"; path: string };
|
||||
|
||||
export interface PluginResource {
|
||||
id: string;
|
||||
source: PluginSource;
|
||||
pluginType?: "claude" | "codex" | "auto";
|
||||
}
|
||||
|
||||
export interface AgentResources {
|
||||
skills: SkillResource[];
|
||||
guidance: GuidanceResource[];
|
||||
subagents: SubagentResource[];
|
||||
hooks: HookResource[];
|
||||
plugins: PluginResource[];
|
||||
runtimeResources: RuntimeResource[];
|
||||
}
|
||||
|
||||
@@ -531,7 +539,7 @@ export interface ProfileSpec {
|
||||
skills: string[];
|
||||
guidance: string[];
|
||||
subagents: string[];
|
||||
hooks: string[];
|
||||
plugins: string[];
|
||||
runtimeResources: string[];
|
||||
};
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user