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

> Learn how the canonical skill per-runtime shim pattern in i-have-adhd separates skill definitions from integration code. Discover the SKILL.md and TypeScript shim approach.

- Repository: [Ayoub Ghriss/i-have-adhd](https://github.com/ayghri/i-have-adhd)
- Tags: deep-dive
- Published: 2026-09-12

---

**The i-have-adhd project separates runtime-agnostic skill definitions from runtime-specific integration code by storing canonical rules in [`SKILL.md`](https://github.com/ayghri/i-have-adhd/blob/main/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`](https://github.com/ayghri/i-have-adhd/blob/main/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:

```typescript
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`](https://github.com/ayghri/i-have-adhd/blob/main/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`](https://github.com/ayghri/i-have-adhd/blob/main/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`](https://github.com/ayghri/i-have-adhd/blob/main/plugin.json), [`opencode.json`](https://github.com/ayghri/i-have-adhd/blob/main/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:

```typescript
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:

```typescript
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:

```typescript
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:

```javascript
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`](https://github.com/ayghri/i-have-adhd/blob/main/skills/i-have-adhd/SKILL.md) | **Canonical skill** containing runtime-agnostic rules and front-matter. |
| [`extensions/i-have-adhd.ts`](https://github.com/ayghri/i-have-adhd/blob/main/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`](https://github.com/ayghri/i-have-adhd/blob/main/plugin.json) / [`opencode.json`](https://github.com/ayghri/i-have-adhd/blob/main/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`](https://github.com/ayghri/i-have-adhd/blob/main/SKILL.md) to power Claude, Codex, Pi, and other assistants without modification.
- The **shim** ([`extensions/i-have-adhd.ts`](https://github.com/ayghri/i-have-adhd/blob/main/extensions/i-have-adhd.ts)) handles state persistence, context injection via `syncContext()`, and UI updates through a unified TypeScript interface.
- **Runtime manifests** ([`plugin.json`](https://github.com/ayghri/i-have-adhd/blob/main/plugin.json), [`opencode.json`](https://github.com/ayghri/i-have-adhd/blob/main/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`](https://github.com/ayghri/i-have-adhd/blob/main/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`](https://github.com/ayghri/i-have-adhd/blob/main/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.