How the Canonical Skill + Per-Runtime Shim Pattern Works in i-have-adhd

The i-have-adhd project separates runtime-agnostic skill definitions from runtime-specific integration code by storing canonical rules in SKILL.md and loading them through a single TypeScript shim that adapts to any supported AI runtime.

The canonical skill + per-runtime shim pattern is an architectural approach used in the ayghri/i-have-adhd repository to maintain a single source of truth for ADHD-friendly output formatting while enabling portability across Claude, Codex, Pi, OpenCode, and other AI coding assistants. This design eliminates duplication by isolating the skill's behavioral logic from the plumbing required to register commands, manage UI state, and inject context into different runtime environments.

Understanding the Architecture

The pattern operates across three distinct layers that separate what the skill does from how each runtime makes it available.

The Canonical Skill Layer

At the foundation lies the canonical skill, defined in skills/i-have-adhd/SKILL.md. This file contains the complete, runtime-agnostic definition of the ADHD-friendly output style, including behavioral rules, front-matter metadata, and formatting instructions. By storing the skill definition as a static Markdown file, the system ensures that behavioral updates require changes in only one location, regardless of how many runtimes ultimately consume it.

The shim accesses this canonical definition using a relative path traversal from the extension directory:

const SKILL_PATH = join(EXTENSION_DIR, "..", "skills", "i-have-adhd", "SKILL.md");
const rules = stripFrontmatter(readFileSync(SKILL_PATH, "utf8"));

The Per-Runtime Shim Layer

The per-runtime shim lives in extensions/i-have-adhd.ts and serves as the universal adapter. Unlike traditional plugins that embed skill logic directly, this shim dynamically loads the canonical skill from SKILL.md and translates it into runtime-specific actions. The same TypeScript file powers every supported runtime—including Pi, OMP, OpenCode, Claude, and Gemini—by abstracting the common operations of state management, context injection, and command registration.

Key functions within the shim include:

  • loadRules() – Reads and parses the canonical skill file
  • restoreState() – Determines initial activation state from persisted storage or flags
  • syncContext() – Injects rules into the model's context window
  • setEnabled() – Toggles the mode and updates UI status badges

Runtime Manifests and Always-On Hooks

Each runtime loads the shim through minimal manifest files (plugin.json, opencode.json) that declare the extension entry point without duplicating logic. Additionally, the hooks/always-on.mjs script provides optional automatic activation by checking for a user-created flag file at ~/.i-have-adhd-always, ensuring the skill activates immediately at session start without manual command invocation.

Execution Flow: From File System to Model Context

The canonical skill + per-runtime shim pattern follows a strict six-step lifecycle every time a session initializes or the user toggles the mode.

1. Loading the Canonical Rules

When the extension initializes, the shim reads the canonical skill definition from disk and strips the YAML front-matter, retaining only the raw behavioral rules:

const rules = stripFrontmatter(readFileSync(SKILL_PATH, "utf8"));

2. Determining Initial State

During the session_start or session_tree event, the restoreState() function (lines 61-68) evaluates four potential activation signals:

  • A persisted state entry (i-have-adhd-state) in the session manager
  • The --adhd command-line flag
  • The presence of the ~/.i-have-adhd-always flag file
  • An internal alwaysOnFlag created by the always-on hook

3. Injecting Rules into Context

When enabled is true and rules have not yet been injected, the syncContext() function (lines 34-47) sends a custom message of type i-have-adhd-rules containing the header and raw rule text:

if (enabled && !injected) {
  pi.sendMessage(
    {
      customType: RULES_MESSAGE_TYPE,
      content: `${RULES_HEADER}\n\n${rules}`,
      display: false,
    },
    { triggerTurn: false },
  );
}

The host runtime's latestMarkerIsActive filter ensures only the most recent rule marker remains active in the context window.

4. Command Registration and Toggling

The shim registers the /i-have-adhd command (lines 87-108) to allow manual toggling. When invoked, the handler flips the enabled boolean, persists the new state, updates the UI status badge, and re-synchronizes the context:

pi.registerCommand("i-have-adhd", {
  description: "Toggle ADHD-friendly output for this session",
  handler: async (args, ctx) => {
    const argument = args.trim().toLowerCase();
    if (argument === "on") {
      setEnabled(true, ctx);   // flips `enabled`, persists state, updates UI
      return;
    }
    // ... additional branches for "off", "status", etc.
  },
});

The command also responds to aliases like /skill:i-have-adhd and stop phrases ("stop adhd mode", "normal mode").

5. UI Synchronization

When ADHD mode is active, the shim calls ctx.ui.setStatus (lines 19-27) to display a visual indicator ("● ADHD ON") in the interface. The hideStatus flag optionally suppresses this feedback for users who prefer minimal UI noise.

6. Always-On Automation

The hooks/always-on.mjs script runs at session startup. If it detects the hidden flag file, it automatically injects the full ruleset by mirroring the shim's syncContext() logic:

if (fs.existsSync(flagPath)) {
  const skillText = fs.readFileSync(skillPath, "utf8");
  await pi.sendMessage({ 
    customType: "i-have-adhd-rules", 
    content: skillText 
  }, { 
    triggerTurn: false 
  });
}

This hook ensures users who always want ADHD-friendly formatting receive it without executing the toggle command.

Key Files in the Implementation

File Role
skills/i-have-adhd/SKILL.md Canonical skill containing runtime-agnostic rules and front-matter.
extensions/i-have-adhd.ts Per-runtime shim that loads the skill, manages state, and interfaces with the host.
hooks/always-on.mjs Optional automation hook that triggers on flag-file presence.
plugin.json / opencode.json Runtime-specific manifests declaring the shim as an extension.

Summary

  • Canonical skill + per-runtime shim separates behavioral definitions from runtime integration, allowing the same SKILL.md to power Claude, Codex, Pi, and other assistants without modification.
  • The shim (extensions/i-have-adhd.ts) handles state persistence, context injection via syncContext(), and UI updates through a unified TypeScript interface.
  • Runtime manifests (plugin.json, opencode.json) declare the extension entry point while the always-on hook (hooks/always-on.mjs) provides automatic activation via file-system flags.
  • Rules are injected as custom messages with type i-have-adhd-rules, ensuring the model receives consistent instructions regardless of which runtime hosts the extension.

Frequently Asked Questions

What is the advantage of separating the skill from the shim?

Separating the canonical skill from the per-runtime shim prevents code duplication and maintenance overhead. When formatting rules need updates, developers modify only SKILL.md rather than editing multiple runtime-specific plugins. This separation also enables immediate portability to new AI runtimes—only a lightweight manifest file is required to adapt the existing shim.

How does the shim know whether to enable ADHD mode at startup?

The restoreState() function in extensions/i-have-adhd.ts checks four sources in sequence: persisted session state (i-have-adhd-state), the --adhd CLI flag, the presence of ~/.i-have-adhd-always, and the alwaysOnFlag set by the hook. If any signal indicates activation, the shim calls syncContext() to inject rules before the user sends their first message.

Can I use this pattern for other types of AI skills?

Yes. The canonical skill + per-runtime shim pattern is generic and applies to any scenario requiring consistent behavioral instructions across multiple AI runtimes. By storing skill logic in a static file (Markdown, JSON, or YAML) and using a thin shim to bridge runtime APIs, you can maintain a single source of truth while supporting diverse host environments like OpenCode, Pi, or Claude Desktop.

Why are the rules injected as a custom message type rather than system instructions?

The shim injects rules using a custom message type (i-have-adhd-rules) with display: false to ensure the instructions appear in the model's context window without cluttering the chat UI. The host runtime's latestMarkerIsActive filter manages these markers efficiently, allowing the system to replace or deactivate rules dynamically when the user toggles modes mid-session.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →