ADR 0020: ALWAYS_ON_SKILLS — non-tier-filterable base skills

Field Value
Status Accepted
Date 2026-05-26
Deciders Project maintainers
Supersedes
Superseded by
Related ADRs ADR-0013 (SkillTemplate enrichment), ADR-0019 (MCP untrustedOutput quarantine skill)

Summary

A subset of base skills — scratchpad-hygiene, reset-context, and quarantining-untrusted-output — are designated ALWAYS_ON_SKILLS and emitted into every adapter's skill set at every conformance tier, including Minimum / Starter. These skills are documentary policies the agent runtime must obey; they cannot be opted out of without compromising the security/hygiene mitigations they enforce.

The invariant is ALWAYS_ON_SKILLS ⊂ BASE_SKILLS. A predicate ALWAYS_ON_SKILL_IDS is used by tier-filtering code paths (baseSkillsForTier and friends) to recognise non-droppable skills. computeFilePlan dedupes by skill id so callers passing both baseSkills: BASE_SKILLS and alwaysOnSkills: ALWAYS_ON_SKILLS do not double-emit.


Context

The 25-pain-point analysis names two mitigations that must be present even at the smallest conformance tier:

Conformance-tier filtering at Starter/Minimum trims the v4 base skill set aggressively — the goal is a 60-second opinionated default emission. But the three skills above correspond to named threats and documented hygiene gates: stripping them at low tiers would mean the smallest-surface projects ship without the very mitigations the pain-point analysis names as highest-impact.

Without an explicit non-droppable category, every new tier-filtering code path has to remember to special-case these skills individually. That coupling is fragile: it is easy to add a fourth always-on skill and forget to update one of the call sites.


Decision

1. Introduce the ALWAYS_ON_SKILLS constant in @aidokit/base-skills

export const ALWAYS_ON_SKILL_IDS = [
  'scratchpad-hygiene',
  'reset-context',
  'quarantining-untrusted-output',
] as const;

export const ALWAYS_ON_SKILLS: readonly SkillTemplate[] =
  BASE_SKILLS.filter((s) => ALWAYS_ON_SKILL_IDS.includes(s.id as never));

2. Maintain the invariant ALWAYS_ON_SKILLS ⊂ BASE_SKILLS

Always-on skills are a labelling of existing base skills, not a separate registry. A unit test asserts that every id in ALWAYS_ON_SKILL_IDS resolves to a real BASE_SKILLS entry.

3. Bypass tier filtering

baseSkillsForTier(tier) and any future tier-filter helper consults ALWAYS_ON_SKILL_IDS and always includes those entries, regardless of the tier's nominal skill-set reduction.

4. Dedupe in computeFilePlan

computeFilePlan dedupes emitted skill entries by id so callers that pass both baseSkills: BASE_SKILLS and alwaysOnSkills: ALWAYS_ON_SKILLS produce the same file plan as callers that pass just one. This keeps third-party adapter integration ergonomic.


Consequences

Positive

Negative

Neutral


Alternatives considered

Alternative 1: Special-case each always-on skill in baseSkillsForTier

Inline a list of skill ids inside the tier-filter function.

Pros

Cons

Reason rejected: high coupling, weak invariant.

Alternative 2: A new "policy skills" category separate from base skills

Introduce a POLICY_SKILLS array distinct from BASE_SKILLS, with its own emit path.

Pros

Cons

Reason rejected: adds vocabulary without solving a problem.


Implementation notes


Validation

This decision is validated when:


References

Internal

External