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:
mvschwarz committed 2026-05-10 02:52:05 -07:00
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;
}
+74 -12
View File
@@ -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[] : [],
},
};
+1 -1
View File
@@ -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 },
];
+11 -3
View File
@@ -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[];
};
}