feat: replace procedure architecture with skills-based approach (CYPACK-996)#1018
feat: replace procedure architecture with skills-based approach (CYPACK-996)#1018Connoropolous merged 25 commits intomainfrom
Conversation
Replace the rigid procedure-based agent session architecture with a flexible skills-based approach. Instead of forced subroutine sequencing with stop/resume between steps, the agent now runs a single continuous session and discovers skills from .claude/skills/ at runtime. Key changes: - Create 5 SKILL.md files (implementation, verify-and-ship, summarize, investigate, debug) from existing subroutine prompts - Add Stop hook to RunnerConfigBuilder to ensure PRs/summaries before session ends (with stop_hook_active guard against infinite loops) - Add deploySkillsToWorkspace() to copy skills to worktree - Append workflow guidance section to system prompt - Remove ProcedureAnalyzer, validation loop, and all subroutine code - Simplify AgentSessionManager (remove procedure completion routing) - Remove ~7000 lines of dead procedure/validation/subroutine code - Delete 7 obsolete test files, update remaining tests
cyrusagent
left a comment
There was a problem hiding this comment.
PR Review — SOLID Principles Analysis
Overall, this is a strong refactor that removes ~7,000 lines of rigid procedure/subroutine/validation code in favor of a simpler, more flexible skills-based approach. The architecture is significantly easier to reason about. Below are observations organized by SOLID principle.
✅ Single Responsibility Principle (SRP) — Mostly Good
Positives:
RunnerConfigBuilderhas a clear, focused responsibility: assembling runner configs for both chat and issue sessions.ToolPermissionResolvercleanly owns tool permission logic without procedure concerns.AgentSessionManageris significantly simplified — it no longer handles procedure routing, validation loops, or subroutine transitions. It's back to its core job: managing session lifecycle and activity posting.- Skills (
SKILL.mdfiles) each encapsulate a single workflow concern (implement, verify, debug, etc.).
Concern:
EdgeWorker.deploySkillsToWorkspace()(line ~4478) is a filesystem operation that doesn't really belong in the EdgeWorker class. EdgeWorker is already a god-class with many responsibilities (webhook handling, session orchestration, prompt assembly, workspace setup, runner management, etc.). Consider extracting this into aSkillDeploymentServiceor at minimum into theGitService/workspace setup layer. This would keep the EdgeWorker focused on orchestration rather than file copying.
✅ Open/Closed Principle (OCP) — Strong Improvement
Positives:
- The skills-based architecture is inherently open for extension. Adding a new skill is just adding a new
skills/<name>/SKILL.mddirectory — no code changes needed. Compare this to the old system where adding a subroutine required modifying the procedure registry, analyzer, and often the EdgeWorker itself. - The stop hook in
RunnerConfigBuilder.buildStopHook()provides a clean extension point via the hook pattern rather than hardcoded control flow.
Concern:
- The stop hook's guidance text is hardcoded as a string literal (lines 343-346 in
RunnerConfigBuilder.ts). If different session types need different stop guidance in the future, this would need to be parameterized. Not a problem today, but worth noting.
✅ Liskov Substitution Principle (LSP) — No Issues
- The
IMcpConfigProvider,IChatToolResolver, andIRunnerSelectorinterfaces inRunnerConfigBuilder.tsare well-designed. Any implementation satisfying these contracts would work correctly as a substitute. AgentSessionManagerconstructor no longer takes optionalProcedureAnalyzerandSharedApplicationServerparams that changed its behavior based on whether they were provided. The simplified constructor produces consistent behavior regardless of context — this is a LSP improvement.
✅ Interface Segregation Principle (ISP) — Good
Positives:
IMcpConfigProvider,IChatToolResolver, andIRunnerSelectorare narrow, focused interfaces that expose only whatRunnerConfigBuilderneeds. This is textbook ISP.- Removing
ProcedureAnalyzerfromAgentSessionManager's constructor eliminates a dependency that forced the class to know about procedure concepts even when they weren't applicable (e.g., Slack chat sessions passedundefined).
⚠️ Dependency Inversion Principle (DIP) — Partial
Positives:
RunnerConfigBuilderdepends on abstractions (IMcpConfigProvider,IChatToolResolver,IRunnerSelector) rather than concrete implementations. Solid DIP.
Concerns:
EdgeWorker.deploySkillsToWorkspace()directly callsreaddir,readFile,writeFile,existsSync, andmkdir(lines 4479-4500). These are concrete filesystem dependencies that make the method untestable in isolation. Injecting a filesystem abstraction (or extracting to a service) would improve testability and follow DIP.- The stop hook inline-imports no external dependencies, which is fine, but the
StopHookInputtype is cast withas(line 331) rather than being typed through the hook system generics. This is a minor type-safety concern.
Additional Observations
-
Dead comment: Line 113 in
EdgeWorker.ts—// Procedures removed — skills-based architecture (CYPACK-996)is a tombstone comment. The git history already captures what was removed and why. Consider removing it. -
Runner type switch chain (lines 210-229 in
RunnerConfigBuilder.ts): Theif/else ifchain for runner type fallback when labels change mid-session is a potential OCP violation. If a new runner type is added, this chain must be extended. A map-based lookup (runnerSessionIdMap) would be more extensible:const sessionRunnerMap: Record<string, RunnerType> = { claude: session.claudeSessionId, gemini: session.geminiSessionId, codex: session.codexSessionId, cursor: session.cursorSessionId, }; // Find existing runner type const existingRunner = Object.entries(sessionRunnerMap) .find(([_, id]) => id && _ !== runnerType)?.[0] as RunnerType | undefined;
-
Skill deployment timing:
deploySkillsToWorkspaceruns during session creation (line 2615). If skills are updated while a session is active, the session won't see the changes. This is probably fine for now since sessions are typically short-lived, but worth documenting. -
Unused
_logparameter:buildStopHooktakes_log: ILoggerbut doesn't use it (line 323). Either use it for debug logging in the hook callback or remove the parameter.
Summary
| Principle | Assessment |
|---|---|
| SRP | ✅ Strong — major improvement via procedure removal. Minor concern: deploySkillsToWorkspace in EdgeWorker |
| OCP | ✅ Strong — skills are inherently extensible without code changes |
| LSP | ✅ Clean — no behavioral surprises from substitution |
| ISP | ✅ Good — narrow, focused interfaces for RunnerConfigBuilder deps |
| DIP |
Overall verdict: This is a well-executed simplification that significantly improves the codebase's adherence to SOLID principles. The 18:1 deletion-to-addition ratio speaks for itself. The concerns raised are minor and none are blocking — they represent opportunities for future improvement rather than current defects.
cyrusagent
left a comment
There was a problem hiding this comment.
Follow-up Review: Skills Availability for External Repositories
After tracing the full skill deployment path, there is a critical gap — skills are only available when Cyrus works on the Cyrus repo itself. They are NOT available when working on external repos.
The Problem
deploySkillsToWorkspace() reads from join(this.cyrusHome, "skills") which resolves to ~/.cyrus/skills/:
// EdgeWorker.ts:4478-4501
private async deploySkillsToWorkspace(workspacePath: string): Promise<void> {
const sourceDir = join(this.cyrusHome, "skills"); // ~/.cyrus/skills/
if (!existsSync(sourceDir)) {
this.logger.debug("No skills directory found at", sourceDir);
return; // ← SILENTLY SKIPS DEPLOYMENT
}
// ...copies SKILL.md files to workspace...
}~/.cyrus/skills/ does not exist. The actual skills live in the Cyrus repo at skills/. There's no build step, install hook, or copy mechanism to get them to ~/.cyrus/skills/.
Why it appears to work for the Cyrus repo
The Cyrus repo has symlinks: .claude/skills/debug -> ../../skills/debug. When Cyrus works on its own repo, the symlinks already resolve to the skills. But for any other repository, the workspace .claude/skills/ directory is empty because deploySkillsToWorkspace() has nothing to copy.
What needs to happen
Option A — Ship skills with the CLI package: Add a postinstall script (or startup-time init in the CLI) that copies the canonical skills to ~/.cyrus/skills/. Also include the skills/ directory in the npm package's files array (apps/cli/package.json currently only ships dist and README.md).
Option B — Resolve skills from the package at runtime: Instead of reading from cyrusHome, resolve the skills directory relative to the edge-worker package (or a new cyrus-skills package) using import.meta.url or __dirname. This way skills are always available from the installed package.
Option C — Embed skills in the build: Bundle the SKILL.md contents into the compiled JS (e.g., as string constants or via the existing copy-prompts build script in edge-worker/package.json).
Secondary Issue: Workflow guidance duplicates skill content
The system prompt hardcodes workflow steps (lines 4350-4357):
systemPrompt +=
"\n\n## Workflow\n\n" +
"Follow this workflow to completion:\n\n" +
"1. **Implement** — Implement the requested changes...\n" +
"2. **Verify & Ship** — Run all quality checks...\n" +
"3. **Summarize** — Post a concise summary...\n\n" +
"Do NOT skip steps. Complete each phase before moving to the next.";This duplicates what's in implementation/SKILL.md, verify-and-ship/SKILL.md, and summarize/SKILL.md. If the skills are properly deployed, the agent discovers them at runtime and this hardcoded guidance becomes redundant (or worse, could drift out of sync). Consider referencing the skills by name instead of duplicating their content in the prompt.
Issue Requirement: "added to the runner config as one of its params"
The issue description states: "These skills should then be added to the runner config as one of its params (see the claude agent sdk docs for more context)". Currently:
AgentRunnerConfigincyrus-corehas noskillsparameter- Skills are deployed purely via filesystem (
deploySkillsToWorkspace()) - The Claude Agent SDK may support a
skillsconfig option that would be the proper way to pass skills to the runner
If the SDK supports it, adding skills to the runner config would be more reliable than filesystem deployment and would solve the cross-repo problem entirely.
…PACK-996) deploySkillsToWorkspace() only read from ~/.cyrus/skills/ which doesn't exist for external repos. Now falls back to skills bundled with the edge-worker package (dist/skills/), ensuring skills are available regardless of which repository the agent is working on. - Bundle skills into edge-worker build via copy-prompts script - Add import.meta.url-based resolution for bundled skills fallback - Replace hardcoded workflow guidance with skills reference in system prompt - Remove tombstone comment - Update prompt assembly tests to match new workflow guidance text
…(CYPACK-996) Instead of copying skills to each workspace's .claude/skills/ directory, package them as a Claude Agent SDK plugin (cyrus-skills-plugin/) and pass via the `plugins` option. The SDK natively discovers skills from plugins, making them available to agents in all repositories. Changes: - Add cyrus-skills-plugin/ with .claude-plugin/plugin.json manifest and symlinked skills directory - Add `plugins` field to AgentRunnerConfig, ClaudeRunnerConfig, and IssueRunnerConfigInput - Wire plugins through ClaudeRunner → SDK query options - EdgeWorker.resolveSkillsPlugins() resolves from ~/.cyrus/ (user) or bundled package (fallback) - Simplify deploySkillsToWorkspace() to only handle user custom skills - Re-export SdkPluginConfig from claude-runner
… (CYPACK-996)
resolveSkillsPlugins() used require("node:fs") which is unavailable in
ESM modules. Replaced with a top-level import of existsSync. Also
removed redundant dynamic import from deploySkillsToWorkspace().
Update the system prompt to map issue types (code changes, bugs, questions, PR review feedback) to the appropriate skill sequences, so the agent knows which skills to invoke based on the context it receives.
SOLID Principles Review — Final PassReviewed the full PR diff (27 files, ~7000 lines removed, skills-based architecture added). Here's the assessment against each SOLID principle: S — Single Responsibility ✅Good:
Minor observation (non-blocking):
O — Open/Closed ✅Good:
L — Liskov Substitution ✅Good:
I — Interface Segregation ✅Good:
D — Dependency Inversion ✅Good:
One finding — dead type in
|
…PACK-996) The procedure/subroutine/validationLoop type definition is unused after removing all procedure logic from AgentSessionManager and EdgeWorker.
…-996) Skills copied into .claude/skills/ inside worktrees would appear in git status and risk being committed to user repos. The plugin system already delivers skills via the SDK without touching the worktree, making this method redundant.
…K-996) CYHOST can now write custom skills to ~/.cyrus/user-skills-plugin/skills/<name>/SKILL.md and they are automatically loaded alongside the bundled default skills. The plugin manifest (.claude-plugin/plugin.json) is auto-scaffolded when the skills directory exists but the manifest is missing. Both bundled and user plugins are loaded together, so user skills supplement rather than replace the defaults.
…(CYPACK-996) - Extract SkillsPluginResolver class from EdgeWorker (SRP): skills plugin resolution, user plugin manifest auto-scaffolding, and buildSkillsGuidance() now live in a dedicated module - Remove stale postProcedureSelectionThought mocks from 7 test files - Update procedure-referencing comments in ChatSessionHandler, RunnerConfigBuilder, and packages/CLAUDE.md - Clean up unused imports (existsSync, mkdirSync, writeFileSync, dirname, fileURLToPath, SdkPluginConfig) from EdgeWorker
Keep skills-based architecture from this branch while incorporating GitLab integration additions from main. Remove procedure code that main added for GitLab (applyPlatformSubroutines, rerouteProcedureForSession) since procedures are replaced by skills. Fix buildAgentRunnerConfig call in GitLab handler to match updated signature (removed singleTurn and disallowAllTools params).
… (CYPACK-996) - Convert all sync fs operations (existsSync, mkdirSync, writeFileSync) to async equivalents (access, mkdir, writeFile) to avoid blocking the event loop during concurrent session starts - Extract scaffolding into ensureUserPluginScaffolded() called once at EdgeWorker startup, making resolve() a pure query with no side effects (Command-Query Separation) - Add discoverSkillNames() to dynamically read available skills from plugin directories, and move buildSkillsGuidance() into the class so the system prompt lists all available skills automatically (OCP) - Reorder resolve() to return user plugin before bundled plugin so user-defined skills take precedence over same-named bundled skills - Add conflict detection that logs when user skills shadow bundled ones - Make buildAgentRunnerConfig() async to support awaiting resolve()
Add HTTP endpoints for CYHOST to manage user skills: - POST /api/update/skill — create or update a skill - DELETE /api/update/skill — remove a skill - GET /api/skills — list all user skills with descriptions Handler writes SKILL.md files with YAML frontmatter to ~/.cyrus/user-skills-plugin/skills/<name>/ following the existing ConfigUpdater pattern (Bearer token auth, ApiResponse format). Includes input validation, skill name sanitization, and frontmatter parsing for list responses.
…(CYPACK-996) Security fixes for production readiness: - Add path traversal protection: validateSkillName() rejects names not matching /^[a-z0-9_-]+$/, resolveSkillDir() verifies the resolved path stays within the skills root directory - Add YAML escaping for frontmatter values to prevent injection via description field containing newlines or special characters - Apply name validation to DELETE handler (was missing — could rm arbitrary directories via ../../../ payload) Also: - Remove unnecessary Dirent type import/cast in list handler - Accept pre-resolved plugins in discoverSkillNames() and buildSkillsGuidance() to avoid redundant filesystem access
…1056) * fix: update Linear tokens on dependent services when config changes When OAuth tokens refresh (at least daily), the new token was persisted to config.json but existing LinearIssueTrackerService instances and AttachmentService continued using the stale token. This adds updateLinearWorkspaceTokens() to the config change handler, which diffs old vs new workspace tokens and calls setAccessToken() on affected trackers. Also adds setLinearWorkspaces() to AttachmentService. Closes CYPACK-1024 * chore: add PR link to changelog entry for CYPACK-1024 * fix: update @anthropic-ai/claude-agent-sdk from ^0.2.88 to ^0.2.89 Version 0.2.88 was unpublished from npm, causing CI to fail with a 404. * fix: downgrade @anthropic-ai/claude-agent-sdk to ^0.2.87 Versions 0.2.88 and 0.2.89 were removed from npm. Pin to 0.2.87, the last stable release.
On startup, DefaultSkillsDeployer copies bundled skills to ~/.cyrus/cyrus-skills-plugin/ if the directory doesn't exist yet. This makes internal skills editable by users while preserving the default set on fresh installs. Existing directories are never overwritten, respecting user modifications. SkillsPluginResolver now resolves the internal plugin from cyrusHome instead of the package's bundled path, with user-skills-plugin still taking precedence for overrides. SOLID: DefaultSkillsDeployer owns one-time deployment (SRP), SkillsPluginResolver owns runtime resolution (SRP), both are independently testable.
Setup skills (cyrus-setup*) are separate and should not be included in the bundled cyrus-skills-plugin. Replace the single symlink to skills/ with individual symlinks for each non-setup skill (debug, f1-test-drive, implementation, investigate, summarize, verify-and-ship). Also fix symlink handling in DefaultSkillsDeployer and SkillsPluginResolver — readdir entries that are symlinks to directories now correctly pass the isDirectory()||isSymbolicLink() check and cp uses dereference:true.
f1-test-drive is an internal testing skill for the cyrus repo itself, not intended for customer use. Remove it from the bundled plugin.
Merge origin/main into cypack-996. Resolves conflicts: - SDK version bumps (claude-agent-sdk ^0.2.89, anthropic-sdk ^0.81.0) - Changelog entries (keep both branch and main entries) - Remove duplicate updateLinearWorkspaceTokens method - Drop unused prevDefaultRunner variable (procedures removed)
The stop hook should only ensure PRs are created, not require a summary post before stopping.
Add buildAgentContextBlock() to EdgeWorker that injects <agent_context> with github/gitlab bot usernames into the system prompt. Update skills to reference these dynamic values instead of hardcoding @cyrusagent. - verify-and-ship: now platform-agnostic (GitHub PR + GitLab MR) - summarize: clarify output auto-streams to Linear, no tool needed - investigate/summarize: reference assignee context for @mentions - glab-mr.md: replace {{gitlab_bot_username}} template variable
Assignee: Payton Webber
Summary
Replaces the rigid procedure-based agent session architecture with a flexible skills-based approach. Procedures (ProcedureAnalyzer, subroutine sequencing, validation loop) are removed in favor of
SKILL.mdfiles delivered via the Claude Agent SDK plugin system. This gives agents more natural control over their workflow while allowing users to customize and add their own skills.Key Changes
debug,implementation,investigate,summarize,verify-and-ship) delivered as.claude-pluginpackages via the SDKpluginsparameter~/.cyrus/cyrus-skills-plugin/so users can customize them. Idempotent — only deploys if the directory doesn't existstop_hook_activeguard to prevent infinite loopsverify-and-shiphandles both GitHub PRs and GitLab MRs based on repository contextGITHUB_BOT_USERNAME,GITLAB_BOT_USERNAME) injected into system prompt via<agent_context>block so skills can reference them dynamically~/.cyrus/user-skills-plugin/with automatic manifest scaffolding and conflict detectionRemoved
ProcedureAnalyzerand all procedure routing logicTesting
DefaultSkillsDeployerunit tests (3 tests)Linear Issue
CYPACK-996