mirror of
https://github.com/LerianStudio/ring
synced 2026-04-21 13:37:27 +00:00
Move OpenCode runtime plugin and installer into Ring monorepo under platforms/opencode/. The installer reads skills, agents, and commands directly from the Ring monorepo's canonical directories (default/, dev-team/, pm-team/, etc.) — zero asset duplication. What's included: - installer.sh: reads from Ring dirs, applies frontmatter/tool transforms, installs to ~/.config/opencode/ - plugin/: TypeScript runtime (RingUnifiedPlugin) with hooks, lifecycle, loaders - src/: CLI (doctor, config-manager) - prompts/: session-start and context-injection templates - standards/: coding standards (from dev-team/docs/) - ring.jsonc: default config with full 86-skill/35-agent/33-command inventory What's NOT included (intentionally): - assets/ directory: eliminated, content comes from Ring monorepo - scripts/codereview/: eliminated, replaced by mithril - using-ring-opencode skill: uses canonical using-ring instead Transforms applied by installer: - Agent: type→mode, strip version/changelog/output_schema/input_schema - Skill: keep name+description frontmatter, body unchanged - Command: strip argument-hint (unsupported by OpenCode) - All: normalize tool names (Bash→bash, Read→read, etc.) - All: strip Model Requirement sections from agents Replaces: LerianStudio/ring-for-opencode repository Generated-by: Gandalf AI-Model: claude-opus-4
223 lines
7 KiB
TypeScript
223 lines
7 KiB
TypeScript
/**
|
|
* Context Injection Hook Factory
|
|
*
|
|
* Handles context injection during session compaction.
|
|
* Provides compact versions of rules, skills, commands, and agents references.
|
|
*/
|
|
|
|
import * as fs from "node:fs"
|
|
import * as path from "node:path"
|
|
import { fileURLToPath } from "node:url"
|
|
|
|
import { type AgentConfig, loadRingAgents } from "../../loaders/agent-loader.js"
|
|
import { loadRingCommands } from "../../loaders/command-loader.js"
|
|
import { loadRingSkills, type SkillConfig } from "../../loaders/skill-loader.js"
|
|
import type { Hook, HookContext, HookFactory, HookOutput, HookResult } from "../types.js"
|
|
|
|
const __filename = fileURLToPath(import.meta.url)
|
|
const __dirname = path.dirname(__filename)
|
|
|
|
/** Root directory (contains agent/, skill/, command/) */
|
|
const PLUGIN_ROOT = path.resolve(__dirname, "../../..")
|
|
|
|
/**
|
|
* Load a prompt file from the prompts directory.
|
|
*/
|
|
function loadPrompt(filename: string): string {
|
|
const promptPath = path.resolve(__dirname, "../../../prompts", filename)
|
|
try {
|
|
return fs.readFileSync(promptPath, "utf-8").trim()
|
|
} catch {
|
|
return ""
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate skills reference from loaded skills.
|
|
* Format: "Available Ring skills: /skill1, /skill2, ... Use 'Skill' tool with skill name to invoke."
|
|
*/
|
|
function generateSkillsReference(skills: Record<string, SkillConfig>): string {
|
|
const skillNames = Object.keys(skills)
|
|
.map((key) => key.replace("ring:", ""))
|
|
.sort()
|
|
.map((name) => `/${name}`)
|
|
.join(", ")
|
|
|
|
if (!skillNames) return ""
|
|
return `Available Ring skills: ${skillNames}\nUse "Skill" tool with skill name to invoke.`
|
|
}
|
|
|
|
/**
|
|
* Generate commands reference from loaded commands.
|
|
* Format: "Ring commands: /cmd1, /cmd2, ... Use skills via Skill tool."
|
|
*/
|
|
function generateCommandsReference(commands: Record<string, unknown>): string {
|
|
const cmdNames = Object.keys(commands)
|
|
.map((key) => key.replace("ring:", ""))
|
|
.sort()
|
|
|
|
if (cmdNames.length === 0) return ""
|
|
return `Ring commands: Use skills via Skill tool. Check /help for full list.\nKey patterns: TDD (test-first), systematic debugging, defense-in-depth validation.`
|
|
}
|
|
|
|
/**
|
|
* Generate agents reference from loaded agents.
|
|
* Format categorizes agents by role prefix for compact representation.
|
|
*/
|
|
function generateAgentsReference(agents: Record<string, AgentConfig>): string {
|
|
const agentNames = Object.keys(agents)
|
|
.map((key) => key.replace("ring:", ""))
|
|
.sort()
|
|
|
|
if (agentNames.length === 0) return ""
|
|
|
|
// Categorize agents by common prefixes
|
|
const devAgents: string[] = []
|
|
const reviewers: string[] = []
|
|
const pmAgents: string[] = []
|
|
const otherAgents: string[] = []
|
|
|
|
for (const name of agentNames) {
|
|
if (name.includes("reviewer")) {
|
|
reviewers.push(name.replace("-reviewer", ""))
|
|
} else if (name.startsWith("pre-dev")) {
|
|
pmAgents.push(name)
|
|
} else if (
|
|
name.includes("engineer") ||
|
|
name === "devops" ||
|
|
name === "sre" ||
|
|
name === "qa-analyst" ||
|
|
name === "frontend-designer"
|
|
) {
|
|
devAgents.push(name)
|
|
} else {
|
|
otherAgents.push(name)
|
|
}
|
|
}
|
|
|
|
const parts: string[] = []
|
|
if (devAgents.length > 0) parts.push(`Dev agents: ${devAgents.join(", ")}`)
|
|
if (reviewers.length > 0) parts.push(`Reviewers: ${reviewers.join(", ")}`)
|
|
if (pmAgents.length > 0) parts.push(`PM agents: ${pmAgents.join(", ")}`)
|
|
if (otherAgents.length > 0) parts.push(`Other: ${otherAgents.join(", ")}`)
|
|
|
|
return `${parts.join("\n")}\nDispatch via dev-cycle or pre-dev workflows.`
|
|
}
|
|
|
|
/**
|
|
* Configuration for context injection hook.
|
|
*/
|
|
export interface ContextInjectionConfig {
|
|
/** Enable compact critical rules */
|
|
injectCompactRules?: boolean
|
|
/** Enable skills reference */
|
|
injectSkillsRef?: boolean
|
|
/** Enable commands reference */
|
|
injectCommandsRef?: boolean
|
|
/** Enable agents reference */
|
|
injectAgentsRef?: boolean
|
|
}
|
|
|
|
/** Default configuration */
|
|
const DEFAULT_CONFIG: Required<ContextInjectionConfig> = {
|
|
injectCompactRules: true,
|
|
injectSkillsRef: true,
|
|
injectCommandsRef: true,
|
|
injectAgentsRef: true,
|
|
}
|
|
|
|
// Load static prompt content (compact-rules.txt is still static)
|
|
const COMPACT_CRITICAL_RULES_CONTENT = loadPrompt("context-injection/compact-rules.txt")
|
|
|
|
/**
|
|
* Compact critical rules for compaction context.
|
|
*/
|
|
const COMPACT_CRITICAL_RULES = COMPACT_CRITICAL_RULES_CONTENT
|
|
? `<ring-compact-rules>
|
|
${COMPACT_CRITICAL_RULES_CONTENT}
|
|
</ring-compact-rules>`
|
|
: ""
|
|
|
|
/**
|
|
* Create a context injection hook.
|
|
*/
|
|
export const createContextInjectionHook: HookFactory<ContextInjectionConfig> = (
|
|
config?: ContextInjectionConfig,
|
|
): Hook => {
|
|
const cfg = { ...DEFAULT_CONFIG, ...config }
|
|
|
|
return {
|
|
name: "context-injection",
|
|
lifecycles: ["session.compacting"],
|
|
priority: 20,
|
|
enabled: true,
|
|
|
|
async execute(ctx: HookContext, output: HookOutput): Promise<HookResult> {
|
|
const contextInjections: string[] = []
|
|
const projectRoot = ctx.directory
|
|
|
|
try {
|
|
// Inject compact critical rules
|
|
if (cfg.injectCompactRules && COMPACT_CRITICAL_RULES) {
|
|
contextInjections.push(COMPACT_CRITICAL_RULES)
|
|
}
|
|
|
|
// Generate and inject skills reference dynamically
|
|
if (cfg.injectSkillsRef) {
|
|
const skills = loadRingSkills(PLUGIN_ROOT, projectRoot)
|
|
const skillsContent = generateSkillsReference(skills)
|
|
if (skillsContent) {
|
|
contextInjections.push(`<ring-skills-ref>\n${skillsContent}\n</ring-skills-ref>`)
|
|
}
|
|
}
|
|
|
|
// Generate and inject commands reference dynamically
|
|
if (cfg.injectCommandsRef) {
|
|
const { commands } = loadRingCommands(PLUGIN_ROOT, projectRoot)
|
|
const commandsContent = generateCommandsReference(commands)
|
|
if (commandsContent) {
|
|
contextInjections.push(`<ring-commands-ref>\n${commandsContent}\n</ring-commands-ref>`)
|
|
}
|
|
}
|
|
|
|
// Generate and inject agents reference dynamically
|
|
if (cfg.injectAgentsRef) {
|
|
const agents = loadRingAgents(PLUGIN_ROOT, projectRoot)
|
|
const agentsContent = generateAgentsReference(agents)
|
|
if (agentsContent) {
|
|
contextInjections.push(`<ring-agents-ref>\n${agentsContent}\n</ring-agents-ref>`)
|
|
}
|
|
}
|
|
|
|
// Add to output context
|
|
if (contextInjections.length > 0) {
|
|
output.context = output.context ?? []
|
|
output.context.push(...contextInjections)
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
data: {
|
|
injectionsCount: contextInjections.length,
|
|
},
|
|
}
|
|
} catch (error) {
|
|
const errorMessage = error instanceof Error ? error.message : String(error)
|
|
return {
|
|
success: false,
|
|
error: `Context injection hook failed: ${errorMessage}`,
|
|
}
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Hook registry entry for context injection.
|
|
*/
|
|
export const contextInjectionEntry = {
|
|
name: "context-injection" as const,
|
|
factory: createContextInjectionHook,
|
|
defaultEnabled: true,
|
|
description: "Injects compact rules, skills, commands, and agents references during compaction",
|
|
}
|