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:
- Prompt injection via MCP / tool output (T18, pain-point #11) —
mitigated by the
quarantining-untrusted-outputskill (ADR-0019). - Context poisoning (T19, pain-point #2) — mitigated by
scratchpad-hygiene(require durable notes; detect brief↔trajectory drift) andreset-context(recovery procedure when drift exceeds reconciliation).
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
- Single source of truth for "skills that must ship at every tier" — adding a fourth always-on skill is one-line.
- Threat traceability — T18 and T19 in
security-model.mdreference the always-on emission as their first line of mitigation; the code predicate is the operational counterpart. - No tier-filter coupling. New tier-aware code paths consult
ALWAYS_ON_SKILL_IDSinstead of carrying special cases.
Negative
- Mitigation is documentary. Same residual risk as ADR-0019:
the skill is text the agent must obey. Documented honestly in
security-model.md§7.13 (T18) and §7.14 (T19). - Slight skill-set bloat at Starter tier. Three extra skills land in the Starter emission. Acceptable: each is short, and each closes a named threat.
Neutral
- No new package.
ALWAYS_ON_SKILLSlives alongsideBASE_SKILLSin@aidokit/base-skills— no taxonomic split.
Alternatives considered
Alternative 1: Special-case each always-on skill in baseSkillsForTier
Inline a list of skill ids inside the tier-filter function.
Pros
- No extra exported constant.
Cons
- Every new tier-aware code path repeats the same hard-coded list.
- Easy to forget when adding a new always-on skill — the failure mode is silent (skill missing at Starter tier, no test failure unless someone wrote a tier-specific assertion).
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
- Strong taxonomic separation between "convention" and "policy".
Cons
- The only operational invariant is "never tier-filtered." A separate category adds taxonomy without functional gain.
- Third-party adapters have to wire two skill arrays where one would do.
Reason rejected: adds vocabulary without solving a problem.
Implementation notes
ALWAYS_ON_SKILL_IDSandALWAYS_ON_SKILLSexported frompackages/base-skills/src/index.ts.computeFilePlandedupes by(template.kind === 'skill', id)composite key.- Unit test: every id in
ALWAYS_ON_SKILL_IDSresolves to a member ofBASE_SKILLS;BASE_SKILLScontains no duplicates of those ids. - Tier-filter unit tests assert that
baseSkillsForTier('starter')contains all three always-on skill ids.
Validation
This decision is validated when:
- [ ]
ALWAYS_ON_SKILL_IDS ⊂ {s.id | s ∈ BASE_SKILLS}holds at build time (enforced by unit test). - [ ]
baseSkillsForTier('starter')returns at least the three always-on skill ids. - [ ]
computeFilePlancalled with bothbaseSkillsandalwaysOnSkillsproduces the same emitted file plan as calling withbaseSkillsalone (modulo the always-on subset already being present). - [ ] Byte-compare fixtures for every adapter at every tier contain the three always-on skill files.
References
Internal
- ADR-0013 —
SkillTemplateenrichment (the schema these skills use). - ADR-0019 — MCP
untrustedOutputflag (introduced thequarantining-untrusted-outputskill that motivated this category). docs/specs/security-model.md§7.13 (T18), §7.14 (T19).packages/base-skills/src/index.ts—ALWAYS_ON_SKILLSexport.
External
- 25-pain-point analysis — pain-points #2 (context poisoning) and #11 (prompt injection via MCP / tool output).