Summary
hooks/AlgorithmNudge.hook.ts carries the always-on skill-routing nudge ("this prompt matches USE WHEN of X — invoke the skill rather than handrolling"). Two independent defects make it silently mute. Both were measured on a current install; neither raises an error, and both fail in the direction that matters: the nudge exists to catch a capability you forgot, so a silent miss is the entire failure mode.
1. Index staleness is measured in time, not in content
loadIndex() rebuilds only when the index is older than INDEX_MAX_AGE_MS (6h):
const stale = !idx || Date.now() - idx.builtAt > INDEX_MAX_AGE_MS;
A skill created just after a rebuild is therefore invisible to routing for up to six hours. Measured: a fresh skill on disk while the index held 58 entries and did not contain it; every other check (skill list, invocation, hygiene gate) passed, so nothing indicated a problem.
Suggested fix — also compare the newest SKILL.md mtime, one readdir plus N stat:
function newestSkillMtime(): number {
let newest = 0;
try {
for (const dir of readdirSync(SKILLS_DIR)) {
try { const t = statSync(join(SKILLS_DIR, dir, 'SKILL.md')).mtimeMs; if (t > newest) newest = t; }
catch { /* no SKILL.md here */ }
}
} catch { /* no skills dir */ }
return newest;
}
const stale = !idx
|| Date.now() - idx.builtAt > INDEX_MAX_AGE_MS
|| newestSkillMtime() > idx.builtAt;
2. matchSkills assumes English morphology
Single-word triggers are matched with an ASCII word boundary:
new RegExp(`\\b${escaped}\\b`).test(p)
Two consequences for any non-English install:
\b is ASCII-only in JS. A trigger containing an accented character never matches. Measured: trigger università, prompt la università di Milano → no match.
- Inflected languages need stems. English adjectives barely inflect, so one listed form covers most prompts. Italian, Spanish, French and German inflect every adjective: a trigger
burocratico does not fire on burocratica or burocratici, and listing every form burns the 1024-char description budget.
Suggested fix — Unicode lookarounds, plus an opt-in trailing * meaning "stem". Backwards compatible: no existing description contains *, so no current behaviour changes.
const isStem = phrase.endsWith('*');
const core = isStem ? phrase.slice(0, -1) : phrase;
if (core.length >= 5) {
const esc = core.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const L = '\\p{L}\\p{N}';
const tail = isStem ? `[${L}]{0,4}` : '';
if (new RegExp(`(?<![${L}])${esc}${tail}(?![${L}])`, 'u').test(p)) { /* match */ }
}
parseUseWhen must stop stripping the trailing * as punctuation.
Verification
Both fixes applied locally and probed with two poles each.
| Case |
before |
after |
SKILL.md written after last rebuild is indexed |
no |
yes |
università trigger vs la università di Milano |
no match |
match |
burocratic* vs questa lettera suona burocratica |
no match |
match |
burocratic* vs la burocrazia italiana (over-trigger control) |
no match |
no match |
| 11 unrelated prompts (over-trigger control) |
0 fire |
0 fire |
Reverting either patch turns its probe red, so the checks discriminate rather than merely pass.
Scope
Affects every install whose operator does not prompt in English, and every install during the window after adding a skill. The first defect is language-independent.
Summary
hooks/AlgorithmNudge.hook.tscarries the always-on skill-routing nudge ("this prompt matches USE WHEN of X — invoke the skill rather than handrolling"). Two independent defects make it silently mute. Both were measured on a current install; neither raises an error, and both fail in the direction that matters: the nudge exists to catch a capability you forgot, so a silent miss is the entire failure mode.1. Index staleness is measured in time, not in content
loadIndex()rebuilds only when the index is older thanINDEX_MAX_AGE_MS(6h):A skill created just after a rebuild is therefore invisible to routing for up to six hours. Measured: a fresh skill on disk while the index held 58 entries and did not contain it; every other check (skill list, invocation, hygiene gate) passed, so nothing indicated a problem.
Suggested fix — also compare the newest
SKILL.mdmtime, onereaddirplus Nstat:2.
matchSkillsassumes English morphologySingle-word triggers are matched with an ASCII word boundary:
Two consequences for any non-English install:
\bis ASCII-only in JS. A trigger containing an accented character never matches. Measured: triggeruniversità, promptla università di Milano→ no match.burocraticodoes not fire onburocraticaorburocratici, and listing every form burns the 1024-char description budget.Suggested fix — Unicode lookarounds, plus an opt-in trailing
*meaning "stem". Backwards compatible: no existing description contains*, so no current behaviour changes.parseUseWhenmust stop stripping the trailing*as punctuation.Verification
Both fixes applied locally and probed with two poles each.
SKILL.mdwritten after last rebuild is indexeduniversitàtrigger vsla università di Milanoburocratic*vsquesta lettera suona burocraticaburocratic*vsla burocrazia italiana(over-trigger control)Reverting either patch turns its probe red, so the checks discriminate rather than merely pass.
Scope
Affects every install whose operator does not prompt in English, and every install during the window after adding a skill. The first defect is language-independent.