# Quentincody Interlinked CLI Enforce

> Quentincody Interlinked Cli Enforce

- Skill: `tomevault-io/quentincody-interlinked-cli-enforce` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add tomevault-io/quentincody-interlinked-cli-enforce`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tomevault-io/quentincody-interlinked-cli-enforce/raw
- Safety review: pending (external: skill-scanner PASS, skillspector FAIL)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: tomevault-io (https://skillmd.com/u/tomevault-io)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/tomevault-io/quentincody-interlinked-cli-enforce

---


# /enforce — Make markdown guidance into enforced harness rules

## Quick start

```
/enforce                              # distill every imperative .md in the project
/enforce AGENTS.md                    # distill just one file
/enforce .claude/skills/tdd/          # distill every .md under a directory
/enforce mattpocock/skills/tdd        # fetch from GitHub (review-mode by default)
/enforce list                         # show what's distilled, grouped by source
/enforce show <id>                    # full detail for one rule
/enforce remove --source <group_id>   # bulk-remove rules from a source
/enforce disable <id>                 # keep on disk but don't enforce
/enforce modify <id> --action ask     # change a single rule's action/severity
/enforce --review                     # distill to a review file; nothing activates yet
/enforce --accept                     # promote a review file to live rules
```

**Manual invocation only.** This skill never auto-fires — not on SessionStart, not on a file-watcher event, not from any hook. The user (or an agent acting on the user's explicit request) types `/enforce ...`. If you are wiring this skill into automation, stop — that violates the design.

**Where output goes.** Live rules: `.interlinked/distilled-rules.json` (pristine, regenerated each run). User mods: `.interlinked/distilled-rules.overrides.json` (removals, disables, modifications — survives re-distillation). The harness watches both files via `watchFile` and reloads automatically within ~2s — there is no `interlinked harness reload` command. If a daemon is in a degraded state and not picking up changes, `interlinked harness restart` is the only recourse.

**Execution order** (distill path; lifecycle verbs jump straight to §14):

1. Pre-flight (§11) → 2. Parse args (§1) → 3. Read overrides (§9) → 4. Read prior `distilled-rules.json` (§3) → 5. Discover / resolve targets (§2) → 6. Read each file once (§3) → 7. Classify paragraphs (§4) → 8. Lexical ladder (§5) → 9. Triggers (§6) → 10. Build rule object (§7) → 11. Apply user modifications (§9) → 12. Resolve conflicts (§10) → 13. Self-checks (§12) → 14. Write output (§8) → 15. Print summary (§13).

---

## Common workflows

### First-time setup on a project

```
/enforce                       # walk the tree, distill what's there, print a report
/enforce list                  # confirm the rule set looks right
                               # harness reloads automatically within ~2s (watchFile)
```

### Adopt a remote skill (review-first)

```
/enforce gh:mattpocock/skills/tdd   # fetched, distilled to review-mode by default
/enforce list                       # inspect what would activate
/enforce --accept                   # promote review → live
```

### A rule is too noisy

```
/enforce show <id>                                       # see what fired and why
/enforce modify <id> --action ask --severity medium      # downgrade
# or, to keep it on disk but stop enforcing entirely:
/enforce disable <id>
```

### Reject a whole source forever

```
/enforce remove --source gh:someone/skills/qa
```

Adds the group to `removed_groups[]`; stays gone across re-distillation. Undo with `/enforce add --source <group_id>`.

### A .md file changed; re-distill

```
/enforce                       # unchanged files skipped via hash; user mods preserved
                               # harness reloads automatically within ~2s
```

### Throw it all away and start fresh

```
rm .interlinked/distilled-rules.json .interlinked/distilled-rules.overrides.json
/enforce
```

---

## What this skill does

Agent-instruction markdown files (AGENTS.md, CLAUDE.md, .clinerules/, .windsurf/rules/, GEMINI.md, etc.) are loaded into the model's context window as hopeful prose. Today the model may or may not follow them. This skill walks the source tree (or just the targets the user named), extracts every concrete imperative from those files, and distills them into typed `GuardRule` entries that the Interlinked harness enforces deterministically at runtime — meaning the agent literally cannot bypass them, regardless of which underlying coding agent is running.

**Distillation, not strict compilation.** The §5 lexical ladder + §6 trigger inference + §12 validation are deterministic — typed inputs, typed outputs, no LLM calls. But §4 paragraph classification (is this prose a hard imperative, soft preference, hedged statement, descriptive context?) requires LLM judgment because there's no formal grammar for "is this an imperative." So the operation is honest extraction-and-codification with provenance, not parser-style mechanical translation. The earlier name "compile" was a generous shorthand; "distill" reflects what's actually happening. The lexical ladder is the deterministic gate: if a paragraph contains no §5 marker, the classifier MUST drop it. The distiller never invents a rule from unmarked prose, and every rule carries a verbatim `source.quote` so the entire pipeline is auditable.

The harness fans rules out across every configured runner via `src/harness/adapters/`. Your job is to produce one canonical artifact at `.interlinked/distilled-rules.json` plus an overrides file at `.interlinked/distilled-rules.overrides.json`. The harness handles distribution.

**Coupling and scope.** This skill produces an artifact whose schema (`GuardRule`, defined at `src/harness/types.ts` in `interlinked-cli`) is Interlinked's. Enforcement at runtime requires a hook engine that understands that schema — Interlinked is the canonical one, and currently the only one expressive enough to encode regex on `tool_input` fields, the six action types, severity, keyword quick-reject, role scoping, and exception patterns. The skill does NOT ship multi-backend adapters (Claude Code permissions, Cursor `.mdc`, Codex deny configs) because every alternative is strictly less expressive — lossy translation creates user confusion ("why doesn't my rule work in X?"). However, distillation itself does not require Interlinked installed: the artifact is a standalone JSON file you can audit, version-control, share, or wire into any compatible engine. See §11 for fallback-mode behavior when `.interlinked/` is missing.

## Invocation patterns (what `/enforce <args>` means)

| Form | Behavior |
|---|---|
| `/enforce` | Walk the whole project — discover all .md files per Step 1, extract from imperative-bearing ones |
| `/enforce AGENTS.md` | Distill only that file |
| `/enforce AGENTS.md CLAUDE.md` | Distill that exact set |
| `/enforce .claude/skills/tdd/` | Distill every .md under the directory |
| `/enforce mattpocock/skills/tdd` | Treat as `gh:mattpocock/skills/tdd` — fetch + distill (review-mode) |
| `/enforce https://raw.githubusercontent.com/.../SKILL.md` | Fetch + distill (review-mode) |
| `/enforce list` | Lifecycle: print rules grouped by source — see §13 |
| `/enforce show <id>` | Lifecycle: full detail for one rule |
| `/enforce remove --source <group_id>` | Lifecycle: bulk-remove from one source |
| `/enforce remove <id>` | Lifecycle: remove a single rule |
| `/enforce disable <id>` | Lifecycle: keep but don't enforce |
| `/enforce enable <id>` | Lifecycle: re-enable |
| `/enforce modify <id> --action ask --severity medium` | Lifecycle: change action/severity |
| `/enforce add --source <group_id>` | Lifecycle: undo a removed group; re-distill to add back |
| `/enforce reset <id>` | Lifecycle: clear all overrides for this rule |
| `/enforce --review` | Distill-then-pause: write to `.interlinked/distilled-rules.review.json`, no activation until accepted |
| `/enforce --accept` | Activate review-mode output |

If the first argument is one of `list`, `show`, `remove`, `disable`, `enable`, `modify`, `add`, `reset`, treat it as a lifecycle verb (jump to §14). Otherwise treat arguments as distill targets.

## Operating principles (NON-NEGOTIABLE)

1. **Manual invocation only.** This skill runs only when the user (or an agent acting on the user's explicit request) types `/enforce ...`. Do not wire it into SessionStart, PreCompact, file watchers, hook events, scheduled jobs, or any other auto-trigger. Surprise enforcement — the model suddenly being unable to do something it could yesterday because a doc changed — is the failure mode this rule prevents.
2. **Verbatim source provenance is required.** Every distilled rule must carry the source file path, line range, and exact verbatim quote. A rule whose `source.quote` does not appear in `source.file` at `source.lines` is hallucination — drop it.
3. **Lexical strength is binding** (see §5 ladder). Don't soften, don't escalate.
4. **Deterministic at evaluation time; semi-deterministic at distill time.** The harness must remain deterministic at runtime — never generate rules that require LLM evaluation to check. The distill step itself is partly LLM-driven (paragraph classification in §4 needs a model to decide "is this prose a hard imperative, soft preference, hedged, or descriptive"), but the lexical ladder (§5) is the deterministic gate: if a paragraph contains no §5 marker, the classifier must drop it. The distiller never invents a rule from unmarked prose. Provenance (§7 `source.quote` verbatim) is what makes the distill-time step auditable rather than opaque.
5. **Default-skip when uncertain.** Far better to skip a borderline imperative than to emit a wrong one.
6. **Never overwrite hand-curated rules.** Output goes only to `.interlinked/distilled-rules.json` and `.interlinked/distilled-rules.overrides.json`. The user's hand-curated rules live in `guard-rules.json` and `guard-rules.local.json`. Never touch those.
7. **Idempotent across runs.** Hash inputs; skip unchanged files; preserve user overrides.
8. **No invention.** If a rule isn't in the source verbatim, it does not exist.

---

## Step 1 — Argument parsing

Parse the invocation arguments. The first arg, if it's one of the lifecycle verbs (`list`, `show`, `remove`, `disable`, `enable`, `modify`, `add`, `reset`), routes to §14. Otherwise:

For each remaining argument, classify it:

| Form | Detection | Resolved as |
|---|---|---|
| Bare name like `AGENTS.md` | exists as file relative to CWD | local file |
| Path with `/` like `.claude/skills/tdd/SKILL.md` | exists as file | local file |
| Directory path like `.claude/skills/tdd/` | exists as dir | walk the dir for `.md` and `.prompt` files |
| Glob like `docs/**/*.md` | contains `*` or `?` | glob expand |
| `<owner>/<repo>` or `<owner>/<repo>/<subpath>` | matches `^[\w.-]+/[\w.-]+(/.+)?$` and not a local path | github shorthand → fetch from `https://raw.githubusercontent.com/<owner>/<repo>/HEAD/<subpath or SKILL.md>` |
| `https://...` or `http://...` | URL prefix | fetch directly |
| `--review`, `--accept`, `--source <x>`, `--action <x>`, `--severity <x>` | flag | parse separately |

Resolution rules:

- For GitHub shorthand: try `<subpath>/SKILL.md`, then `<subpath>` directly, then `<subpath>/AGENTS.md`, in that order.
- For URL or shorthand: only allow hosts in the default allowlist — `github.com`, `raw.githubusercontent.com`, `gitlab.com`. Refuse any other host with a clear error.
- For URL or shorthand: default to `--review` mode (write to `distilled-rules.review.json` instead of `distilled-rules.json`); local paths can distill straight through unless `--review` was passed.
- **For local paths (file or directory): refuse if the resolved absolute path is outside the current repo / project root.** Find the project root by walking CWD upward to the nearest `.git/` (or treat CWD itself as the root if there is no git repo). If the target's resolved absolute path doesn't have that root as a prefix, abort with a clear message: `"<target> is outside the current project (<root>). cd into that project and re-run /enforce."` Reasoning: the artifact + overrides files live under the *target's* `.interlinked/`, the harness running in this session only watches the current project's `.interlinked/`, and writing into a sibling project from this session is the kind of cross-project surprise the skill exists to prevent.
- If no targets resolve, fall back to project walk per Step 2.

Print the resolved target list before doing any work. Form: `Distilling: AGENTS.md, CLAUDE.md, gh:mattpocock/skills/tdd (fetched sha256:abc…)`

---

## Step 2 — Discover sources (no-arg or directory walk)

When no argument is given, walk these locations and record each file's absolute path, kind, and SHA-256 hash.

### 2a — Project tree (CWD upward to git root)

For each ancestor directory from CWD up to the repo root (the directory containing `.git/`), look for:

| Filename / pattern | Kind | Notes |
|---|---|---|
| `AGENTS.override.md` | imperative | Highest project precedence |
| `*.local.md` (e.g. `CLAUDE.local.md`) | imperative | Personal, gitignored, beats shared |
| `AGENTS.md` | imperative | Cross-tool source of truth |
| `AGENT.md` | imperative | Singular variant (Amp/community) |
| `CLAUDE.md` | imperative | Claude Code |
| `GEMINI.md` | imperative | Gemini CLI |
| `WARP.md` | imperative | Warp legacy |
| `.github/copilot-instructions.md` | imperative | Repo-wide Copilot |
| `.github/instructions/*.instructions.md` | imperative | Path-scoped via frontmatter `applyTo` |
| `.cursor/rules/*.mdc` | imperative | Cursor; frontmatter `globs`/`description` is machine-readable |
| `.cursorrules` | imperative | Cursor legacy |
| `.clinerules` | imperative | Cline single-file legacy |
| `.clinerules/*.md` | imperative | Cline modular |
| `.windsurfrules` | imperative | Windsurf legacy |
| `.windsurf/rules/*.md` | imperative | Windsurf modular |
| `.continue/rules/*.md` | imperative | Continue.dev — frontmatter `globs`/`alwaysApply` |
| `.augment/rules/*.md` | imperative | Augment |
| `.tabnine/guidelines/*.md` | imperative | Tabnine |
| `.tabnine/guidelines.md` | imperative | Tabnine single-file |
| `.kilocoderules` | imperative | Kilo Code |
| `.claude/skills/*/SKILL.md` | scan-only | See §2c |
| `.codex/skills/*/SKILL.md` | scan-only | See §2c |
| `~/.claude/skills/*/SKILL.md` | scan-only | See §2c |
| `CONVENTIONS.md` | imperative-likely | Aider-style |
| `code_review.md` | imperative-likely | Often referenced from AGENTS.md |
| `CONTRIBUTING.md` | mixed (scan only) | Pull only paragraphs with hard imperatives |
| `SECURITY.md` | mixed (scan only) | Pull only paragraphs with hard imperatives |
| `STYLEGUIDE.md` | mixed (scan only) | Pull only paragraphs with hard imperatives |
| `PLANS.md` | scan only | Mostly procedural |

### 2b — User home (global rules)

Look in `~/.claude/`, `~/.codex/`, `~/.gemini/`, `~/.config/copilot/`, `~/.continue/`, `~/.windsurf/` for the same patterns. Treat global rules as **lower precedence** than project rules unless they appear in `*.override.md` form.

### 2c — Skills as scan-only sources

SKILL.md files are mostly procedural (capability bundles). They are scan-only: extract paragraphs that hit the §4a/§4b lexical markers (`MUST NOT`, `bans`, `forbids`, `never`, `MUST`, `always`); ignore the rest. Every rule from a skill body has its `group_id` formed as `skill:<skill-name>` (extracted from the SKILL.md frontmatter `name` field) instead of `local:` or `gh:`.

**Skill rules ARE scope-gated at runtime** via `active_when.skill`. The harness's `SessionTrajectory.active_skills` map is populated by `interlinked skill enter <name>` (called from a slash-command preamble) and read by the active-when evaluator. A distilled skill rule with `active_when.skill = "<skill-name>"` is **dormant** unless that skill is currently active in the session. This means:

- **Distill at full strength** — a skill imperative phrased `Never X` distills to `action: "block"` per §5a, not the previous safety-downgrade to `ask`. The runtime scope makes the strength safe.
- **Composes with §10 conflict resolution** — scope-disjoint rules don't conflict; importing N skill rules without scope used to yield O(N²) collisions, with `active_when.skill` populated they only collide within the same skill or with always-on rules.
- **Skill-author opt-in cost is one line** — the slash-command preamble adds `interlinked skill enter <skill-name>` (and a matching `leave` on completion). Skills that don't opt in still distill correctly but their rules are always-on (`active_when` omitted), which is the pre-active_when behavior.

See `docs/design/harness-active-when-scoping.md` for the full design and §7 below for distiller population rules.

The `skill:` group_id is also used for lifecycle ops (`/enforce remove --source skill:migrate-to-shoehorn`).

### 2d — Skip list (DO NOT extract from these — confirm kind, then skip)

| File | Why skipped |
|---|---|
| `SOUL.md`, `IDENTITY.md`, `STYLE.md`, `USER.md`, `HERMES.md` | Persona/voice. Not enforceable as hooks. |
| `MEMORY.md` | Memory index; agent's concern, not the harness's. |
| `HEARTBEAT.md`, `BOOTSTRAP.md` | Lifecycle/initialization, not gating. |
| `ARCHITECTURE.md`, `DESIGN.md`, `RUNBOOK.md`, `TESTING.md`, `BUILD.md`, `DEPLOYMENT.md`, `RELEASE.md`, `TROUBLESHOOTING.md`, `CONTEXT.md` | Descriptive context, not imperative. |
| `PRD.md`, `SPEC.md`, `ROADMAP.md`, `TASKS.md`, `TODO.md` | Forward-looking. |
| `README.md` | Human-facing overview. |
| `TOOLS.md` | Tool inventory. |
| `.agent.md`, `.prompt.md`, `*.prompt`, `.github/agents/*.agent.md`, `.github/prompts/*.prompt.md` | Capability bundles — same treatment as SKILL.md per §2c. `*.prompt` covers bare-extension role/capability prompts (e.g. SwarmForge `<role>.prompt`). |
| Any `SKILL.md` whose frontmatter `name` is `enforce` | Self-reference. The distiller must not distill its own imperatives — they describe how to distill, not what the agent should do. Drop the file silently regardless of which install path it lives at (`skills/`, `.claude/skills/`, `.codex/skills/`, `.interlinked/skills/`, `.gemini/extensions/`, `.github/skills/`, etc.). |
| Any `SKILL.md` whose frontmatter `name` is `tdd`, starts with `tdd-`, or ends with `-tdd`; any path matching `**/tdd/SKILL.md` or `**/*-tdd/SKILL.md` | TDD enforcement is owned by the harness's native primitives — `tdd_new_file_gate`, `tdd_cycle_violation`, `tdd_regression`, `tdd_commit_gate`, `tdd_green_confirmation` — driven by the deterministic `tdd_cycles` state machine in `SessionTrajectory`. Distilling a competing TDD skill (Matt Pocock's, gstack's, anyone else's) would either over-fire (always-on rules duplicating our gates) or shadow (action downgraded to `ask`). Drop these files silently. If a project legitimately needs a different TDD policy, edit `structural_checks.test_first_mode` in `guard-rules.local.json`, not via /enforce. |

After discovery, **print the file inventory** before any extraction so the user can see the surface.

---

## Step 3 — Read each file once

Use the Read tool with the full path. Cache contents. SHA-256 each file.

If `.interlinked/distilled-rules.json` already exists, compare each file's hash to the previous run's `source_hashes` map. **Unchanged files: skip extraction; copy their previous distilled rules verbatim into the new output.** Only re-extract files whose hash changed or which are new.

**Subset distillation preserves other sources.** When the user runs `/enforce <single-target>` (a path, a directory, a remote URL), the distiller MUST merge with the prior `distilled-rules.json`: rules from sources NOT in the current invocation are copied through unchanged, with their original `source_hashes` entries preserved. This is the natural extension of the unchanged-files rule: out-of-scope sources are unchanged by definition. Only `/enforce` (no arg, project walk) is allowed to fully regenerate. If you need a hard rebuild from a subset invocation, the user must `rm .interlinked/distilled-rules.json` first — the §"Throw it all away and start fresh" workflow.

For files with frontmatter, parse it and use machine-readable fields directly:

- **Cursor `.mdc`**: `globs` / `description`
- **Continue `.continue/rules/*.md`**: `globs` / `alwaysApply` / `description`
- **Copilot `.github/instructions/*.instructions.md`**: `applyTo`
- **SKILL.md**: `name` (becomes part of `group_id`), `description`

Frontmatter scope is machine-readable — do not re-extract it from the prose.

---

## Step 4 — Per-paragraph classification

Iterate paragraph-by-paragraph (split on blank lines and heading boundaries). For each paragraph:

| Paragraph kind | Action |
|---|---|
| **Hard imperative** with concrete trigger | extract |
| **Soft preference** (`should`, `prefer`, `usually`) | extract as advisory |
| **Hedged statement** (`we usually try to`, `ideally`, `if possible`) | skip → log to `skipped[]` |
| **Description / context** | skip silently (not imperative) |
| **Narrative / persona** | skip silently (not enforceable) |
| **Procedure / step-by-step** | skip — agent guidance, not gates. Exception: a single step phrased as `you MUST run tests first` extracts as one rule. |
| **Architecture / dependency-graph fact** | skip silently |
| **Forbidden tool / command list** | extract per item |
| **Required tool / command list** | extract as block-on-inverse |

---

## Step 5 — Lexical strength → action ladder (BINDING)

Apply mechanically. Do not adjust. If multiple markers appear in one paragraph, use the strongest.

### 5a — `block` (severity: critical or high)

Lexical markers (case-insensitive unless explicitly ALL CAPS, which strengthens):

- `MUST NOT`, `must never`, `never`, `forbidden`, `prohibited`, `not allowed`, `do not ever`, `may not`, `shall not`, `banned`, `outlawed`, `under no circumstances`, `at no time`, `bans`
- Headers: `CRITICAL:`, `BLOCKING:`, `FATAL:`, `DO NOT:`

**Severity:** `critical` if marker is `CRITICAL`, `MUST NOT`, `never`, `forbidden`, `prohibited`, `shall not`, `under no circumstances`. Otherwise `high`.

### 5b — `block` via positive form (block on inverse trigger)

Positive imperatives with concrete trigger:

- `must`, `MUST`, `required`, `is required`, `is mandatory`, `has to`, `shall`, `always`, `every time`, `before X you must Y`

For these, the trigger fires when the **missing precondition** is detected. Severity: `high`.

If you cannot model the precondition (no observable session state), downgrade to `ask` and note the gap in `distilled_action_reason`.

### 5c — `ask` (severity: medium)

- `should not`, `avoid`, `don't`, `prefer not to`, `try not to`, `discouraged`

The `ask` primitive prompts the user before allowing. It collapses to `deny` on runners that lack confirmation (Copilot CLI, Codex). The harness handles that translation.

### 5d — `advisory` (severity: low; surfaces only under `verify --all-checks`)

- `should`, `prefer`, `usually`, `consider`, `recommend`, `ideally`, `try to`, `encourage`, `we like to`, `aim to`

Distilled as `action: "warn"` with `enabled: true`. The verify pipeline gates these per its own advisory list.

### 5e — SKIP (do not extract)

- `we may`, `we might`, `we sometimes`, `possibly`, `feel free to`, `if you want`, `optionally`, `maybe`
- Any imperative with no concrete trigger (no tool, no file glob, no command regex, no session-state predicate)
- Aspirational language without an observable signal
- Anything where you cannot construct a verbatim source quote

### 5f — Action × trigger compatibility (binding)

`block` and `ask` are decisions made *before* the tool call runs — they require `trigger: "PreToolUse"`. The harness has no post-rule evaluation: a PostToolUse rule cannot block, undo, or ask permission for a write that already happened. So:

| Action | Allowed triggers |
|---|---|
| `block`, `ask`, `soft_block`, `rewrite` | `PreToolUse` only |
| `warn` | `PreToolUse` or `PostToolUse` |

When the imperative's signal is only observable post-write (e.g., scanning produced file content for a pattern), the rule MUST distill as `warn`, not `ask`. Use `PostToolUse` + `warn` for advisories about content that just landed; reserve `ask` for `PreToolUse` checks where the agent can still back out. The trigger cookbook below routes `Don't commit secret Z to source` through PostToolUse + content regex precisely because that's the one place ask-vs-warn doesn't matter — content scanning IS warning, not blocking.

---

## Step 6 — Trigger extraction (real GuardRule schema)

For each imperative, produce the trigger fields. **If you cannot, downgrade to `advisory`** — never emit a `block` or `ask` rule with no observable trigger.

The harness's `GuardRule` shape (from `src/harness/types.ts`):

```ts
interface GuardRule {
  id: string;
  enabled: boolean;
  trigger: "PreToolUse" | "PostToolUse" | "both";
  tool_match: string[];                      // tool names; "*" for all
  action: "block" | "warn" | "rewrite" | "soft_block" | "ask";
  patterns: RulePattern[];                   // see semantics below
  reason: string;                            // shown to the agent
  suggestion?: string;
  severity: "critical" | "high" | "medium" | "low";
  category?: string;
  applies_to_roles?: AgentRole[];
  keywords?: string[];                       // PreToolUse quick-reject tokens
}

interface RulePattern {
  field: string;                             // dot-path into tool_input
  regex: string;
  flags?: string;                            // default "i"
  negate?: boolean;                          // exception when true
}
```

Distilled rules ALSO carry a `source` sidecar field — see §7. The harness ignores unknown fields; the CLI lifecycle ops use them.

### Pattern semantics (binding)

The runtime evaluator splits `patterns[]` by `negate`:

- **Positive patterns (`negate` absent or `false`) — OR.** ANY one positive pattern matching makes the rule fire. With zero positive patterns, the OR is vacuously true: the rule fires whenever its `tool_match` matches.
- **Negated patterns (`negate: true`) — exceptions.** If ANY negated pattern matches, the rule is suppressed for that call.

The combined contract: a rule fires when (at least one positive matches OR there are no positive patterns) AND no negated pattern matches.

Practical consequence: **you cannot AND two positive patterns together.** If your imperative needs both "this file path" AND "this content", encode it as one positive pattern (a regex that captures both signals on one field) plus optional negated exceptions. Don't write the rule as two positive patterns and assume they intersect — that gives you a strict OR. The trigger cookbook below picks the smallest field that captures intent; when in doubt, single-pattern rules are easier to reason about than multi-pattern ones.

### Trigger inference cookbook

| Imperative shape | Distilled fields |
|---|---|
| "Never run X" / "Don't use X" | `trigger: "PreToolUse"`, `tool_match: ["Bash"]`, `patterns: [{ field: "command", regex: "<X>" }]`, `keywords: ["<token>"]` |
| "Don't edit files in path/" | `trigger: "PreToolUse"`, `tool_match: ["Edit","Write","MultiEdit","apply_patch"]`, `patterns: [{ field: "file_path", regex: "<glob-as-regex>" }]` |
| "Always do X before Y" | trigger fires on Y; harness session state required (see ‡) |
| "Use X instead of Y" | `tool_match: ["Bash"]`, `patterns: [{ field: "command", regex: "<Y>" }]`, `suggestion: "use X"` |
| "Don't commit secret Z to source" | `trigger: "PostToolUse"`, `tool_match: ["Edit","Write","MultiEdit","apply_patch"]`, `patterns: [{ field: "content", regex: "<Z>" }]` |
| MCP tool prohibition | `trigger: "PreToolUse"`, `tool_match: ["<exact-mcp-tool-name>"]`, `patterns: []` (exact `tool_match` alone fires the rule via the vacuous-OR; `field: "*"` + `.*` would fail self-check #3) |
| Tool-class prohibition | `tool_match: ["Bash"]`, `keywords: [<token>]`, `patterns: [{ field: "command", regex: "<pattern>" }]` |

‡ Sequential preconditions ("Always X before Y") are not directly representable in `GuardRule`. Emit a Pass 2 `policy.md` entry (see §15.2) where `trigger_signal` describes the precondition in natural language; the Tier 2 LLM gate evaluates it from trajectory. Do NOT downgrade to `ask` in `distilled-rules.json` — `ask` is removed per §15.1 (PreToolUse user-prompts created fatigue; the Tier 2 gate handles the same job without interrupting).

### Pattern hygiene (mandatory)

- Use `\b` word boundaries — never bare `git` (matches `gitlab`, `git-credential`).
- Anchor where it makes sense: `^git\s+push\b`.
- Case-insensitive for SQL keywords: `flags: "i"` on patterns matching `DROP\s+TABLE`.
- Reject any pattern that matches the empty string (`new RegExp(p).test("")`).
- Reject catastrophic-backtracking constructs: `(.*)*`, `(.+)+`, nested unbounded quantifiers.
- For multi-tool rules, list every tool: `tool_match: ["Edit", "Write", "MultiEdit", "apply_patch"]` (`apply_patch` is Codex CLI's edit tool — include it for cross-runner coverage).
- Glob → regex translation: `db/migrations/**` becomes `^db/migrations/.*` (anchored at field start).

---

## Step 6.5 — Session predicates and active-when axes the distiller can consume

Most real AGENTS.md content is sequential ("always run tests before commit," "always read before edit") or scoped ("during /ship, never X," "while migrate-to-shoehorn is active, …"). The distiller has two mechanisms for these:

**(A) Typed `active_when` axes (preferred when applicable).** Recognized prose patterns map directly to a typed scope axis on the distilled rule. Wired into the harness; deterministic at runtime; composable.

**(B) `session_predicate` escape hatch.** For prose patterns that don't match a typed axis, emit a `predicate` entry inside `active_when` with the predicate name and args. **Predicate-using rules must still set `action: "ask"`** so they degrade to a no-op on harness builds that don't recognize the predicate name (the harness fails-safe — unknown predicates keep the rule dormant, never fire).

| Prose phrase | Distilled as | Reads from | Notes |
|---|---|---|---|
| "during /<skill>", "while <skill> is active", source is a SKILL.md body | **`active_when.skill = "<skill-name>"`** (typed axis) | `SessionTrajectory.active_skills` | Default for every rule extracted from a `<name>/SKILL.md` body. Skill-author opt-in via `interlinked skill enter` preamble. |
| "after running /ship", "after the user invoked X" | **`active_when.after_command = { pattern, window_steps }`** (typed axis) | `SessionTrajectory.commands_run` (last N) | Default window 10. Pattern is a regex matched against recent command entries. |
| "only when editing files matching X" | **`active_when.file_scope = "<regex>"`** (typed axis) | `event.tool_input.file_path` | AND-ed with `rule.patterns`; an extra path filter beyond `tool_match`. |
| "only on Codex" / "only on Claude" / model-overlay file home | **`active_when.agent_source = "<runner>"`** or `active_when.overlay = "<runner>"` (typed axes) | `event.agent_source` | Use `overlay` when the source is a `model-overlays/<runner>.md` file; use `agent_source` when prose explicitly names the runner. Both resolve identically in v1. |
| "always run tests before <X>" | `predicate: { name: "tests_passed_recently", args: { window_steps: N } }` | `test_runs.last_pass.at_step` vs `tool_call_count` | N defaults to 5. Action MUST be `"ask"`. |
| "after running tests" | `predicate: { name: "tests_run_in_session" }` | `test_runs.size > 0` | Action MUST be `"ask"`. |
| ~~"while RED, don't <X>"~~ | ~~`predicate: { name: "tdd_state", args: { value: "red" } }`~~ | ~~`tdd_cycles[file].state`~~ | **Harness-internal only — not emitted by the distiller.** TDD enforcement is owned by the harness's native primitives (see §2d skip-list entry for TDD skills). The `tdd_cycles` state machine exists in the runtime, but distilled rules MUST NOT reference it — drop any imperative that would. If you need a TDD policy change, edit `structural_checks.test_first_mode`, not /enforce. |
| "before pushing" | `predicate: { name: "last_command_was", args: { pattern: "^git\\s+push\\b" } }` | `commands_run` (last entry) | Negate-form: trigger when X happens AND last_command was NOT a push. Action MUST be `"ask"`. |
| "before editing, read the file" | `predicate: { name: "file_read_in_session" }` | `files_read` set | Trigger Edit when target file isn't in the set. Action MUST be `"ask"`. |
| "after seeing N consecutive failures" | `predicate: { name: "consecutive_failures", args: { tool, n } }` | `consecutive_tool_failures` | Self-throttling rules. Action MUST be `"ask"`. |
| "during cleanup" / "after compaction" | `predicate: { name: "last_event", args: { name: "PreCompact" } }` | `tool_sequence` | Phase-scoped. Action MUST be `"ask"`. |

**If a predicate isn't in this table, the imperative's "always X before Y" form must route to a Pass 2 `policy.md` entry per §6 ‡ + §15.1** — and the imperative's "no observable session-state primitive" gap should be recorded in the Pass 2 entry's `rationale` so future-you can spot which predicates would benefit from being added to the table (so the LLM-side judgment can be replaced with a deterministic predicate later).

**Schema addition (§7) when a predicate is used:**

```json
"session_predicate": {
  "name": "tests_passed_recently",
  "args": { "window_steps": 5 }
}
```

The harness evaluator must short-circuit `allow` when the predicate is satisfied (the precondition holds) and fall through to the rule's `action` when it's not. Until the harness side wires this up — track via the implementation tracker — distilled rules carrying `session_predicate` MUST also carry `action: "ask"` and the predicate description in `reason`, so they degrade gracefully on harness builds that don't understand the field.

---

## Step 7 — Build the distilled rule (one entry per imperative)

The distilled rule object is a real `GuardRule` with a `source` sidecar field added. The harness ignores `source`; the CLI uses it for lifecycle operations.

```json
{
  "id": "enforce-<group-stem>-<short-kebab-summary>",
  "enabled": true,
  "trigger": "PreToolUse",
  "tool_match": ["Bash"],
  "action": "block",
  "patterns": [
    { "field": "command", "regex": "^git\\s+push\\b.*\\bmain\\b" }
  ],
  "reason": "BLOCKED by AGENTS.md:42 — \"Never push to main without code review.\"",
  "suggestion": "Open a PR and request review, then merge through the standard flow.",
  "severity": "critical",
  "category": "distilled-from-md",
  "keywords": ["git"],
  "source": {
    "group_id":     "local:AGENTS.md",
    "group_label":  "AGENTS.md",
    "file":         "AGENTS.md",
    "lines":        [42, 42],
    "quote":        "Never push to main without code review.",
    "lexical_marker": "Never",
    "marker_class": "block-direct"
  },
  "distilled_action_reason": "lexical 'Never' → action=block per §5a",
  "confidence": 0.95
}
```

**ID slug rule:** `enforce-<group-stem>-<short-kebab-summary>`. Group-stem is derived from `group_id` (drop scheme prefix, replace `/`/`.`/`:` with `-`, lowercase). Summary is ≤6 words from the imperative's intent, kebab-case.

**`group_id` schemes:**

| Scheme | Format | Example |
|---|---|---|
| `local:` | `local:<path>` — repo-relative when a git root exists, otherwise CWD-relative | `local:AGENTS.md`, `local:.clinerules/style.md` |
| `home:` | `home:<home-relative-path>` | `home:.claude/CLAUDE.md` |
| `gh:` | `gh:<owner>/<repo>/<subpath>` | `gh:mattpocock/skills/tdd` |
| `url:` | `url:<host><path>` | `url:example.com/foo.md` |
| `skill:` | `skill:<skill-name>` (from SKILL.md frontmatter) | `skill:tdd`, `skill:grill-me` |

Skill-sourced rules ALWAYS use the `skill:` scheme; they carry their physical install path in `source.file` but are grouped by skill-name so cross-install moves don't fragment the group.

**`confidence`:** 0.95 for clean direct-prohibition. 0.85 for positive-form. 0.7 for cases where the trigger required interpretation. Below 0.7 → downgrade to advisory.

**`active_when` population (binding):** every distilled rule with a known scope MUST carry an `active_when` field per the table below. Rules from project-wide imperative sources (root CLAUDE.md, AGENTS.md, etc.) omit `active_when` (always-on). Rules from skill bodies, model-overlay files, or path-scoped sources populate it deterministically. Population is purely structural — no LLM judgment.

| Source location | `active_when` populated as |
|---|---|
| `<path>/AGENTS.md`, `<path>/CLAUDE.md`, `<path>/AGENTS.override.md`, `*.local.md`, `.clinerules/`, `.cursor/rules/`, `.continue/rules/`, `.windsurf/rules/`, `.augment/rules/`, `.tabnine/`, `.kilocoderules` | omitted (always-on) — these are the project-wide gates |
| `<skill-name>/SKILL.md` body (any install path) | `{ skill: "<skill-name>" }` — skill-name from frontmatter |
| `model-overlays/<runner>.md` (or any sibling persona-by-runner directory) | `{ overlay: "<runner>" }` |
| `.github/instructions/<file>.instructions.md` with frontmatter `applyTo: "<glob>"` | `{ file_scope: "<glob-as-regex>" }` |
| `.cursor/rules/*.mdc` with frontmatter `globs: [...]` | `{ file_scope: "<joined-globs-as-regex>" }` |
| `.continue/rules/*.md` with frontmatter `alwaysApply: false` + `globs: [...]` | `{ file_scope: "<joined-globs-as-regex>" }` |
| Skill body imperative referencing "after running /<X>" or "after the user invoked X" | merge `{ after_command: { pattern: "^/<X>\\b", window_steps: 20 } }` into the existing scope |
| Skill body imperative referencing TDD state | **not emitted** — TDD is owned by harness primitives; rule is dropped per §2d skip list |

When multiple axes are inferred from one source (e.g., a skill body that also mentions "after /ship"), all axes are AND-ed in `active_when`. The distiller emits the union; `confidence` drops by 0.05 per inferred axis beyond the first.

**`applies_to_roles` population (binding).** A source file is *role-scoped* when its basename is `<role>.prompt` / `<role>.agent.md`, or its body opens with `You are the <role>.`. The harness `AgentRole` type (`src/harness/types.ts`) is exactly `"lead" | "worker" | "subagent" | "unknown"`:

- If `<role>` is one of those four, set `applies_to_roles: ["<role>"]` on every rule distilled from that file.
- If `<role>` is NOT — e.g. a bespoke multi-agent cohort such as SwarmForge's `architect` / `coder` / `refactorer` / `reviewer` / `specifier` — the harness has no axis to scope the rule, so a Pass 1 rule from one role's prompt applies to *every* agent. That is a correctness bug, not noise: `refactorer.prompt`'s "Do not run mutation tests" emitted globally directly contradicts `reviewer.prompt`'s "Run … mutation tests." Such imperatives MUST NOT be written to `distilled-rules.json`. Route them to Pass 2 (`policy.md`) with the role named in `trigger_signal` ("…when the acting agent's role is `refactorer`"); the Tier-2 gate resolves role from trajectory. If Pass 2 is unavailable (local-only mode), log to `skipped.report.md` with skip class `skip-role-unmodellable`. §10 conflict resolution does NOT catch this — only the prohibition carries a lexical marker, so the contradicting permission never distills and no conflict pair forms.

**Schema attribution.** The shape above is the Interlinked harness's `GuardRule`, defined at `src/harness/types.ts` in the `interlinked-cli` package. The four sidecar fields (`source`, `distilled_action_reason`, `confidence`, `user_modified`) are the only additions this skill makes; the harness ignores them at evaluation. The `active_when` field is a real `GuardRule` field (added 2026-04 alongside the active-when scoping work) — the harness reads and evaluates it at runtime via `evaluator/active-when.ts`. Other hook engines that want to consume the same artifact must implement matching evaluation semantics — regex on `tool_input` fields, the six action types (`block` / `warn` / `rewrite` / `soft_block` / `ask` / advisory), severity, keyword quick-reject, role scoping, exception patterns, AND active_when scope evaluation. The skill targets `GuardRule` because it's the most expressive open enforcement primitive today; downgrading to a less-expressive runtime (Claude Code's `permissions.deny[]`, Cursor `.mdc` globs

…(truncated)
