397777089e
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
98 lines
3.3 KiB
JavaScript
98 lines
3.3 KiB
JavaScript
/**
|
|
* TodoWrite vs Skill transcript verifier — router-gate v4 spec §3.9 + v4.1 (Direction 4).
|
|
* Pure: ловит TodoWrite claims "invoked X skill" без actual Skill tool call + erasure.
|
|
* v4.1: hard sync — completed item claiming skill but not invoked → block next mutating.
|
|
*/
|
|
|
|
export const SKILL_MENTION_PATTERNS = [
|
|
/superpowers:[a-z-]+/g,
|
|
/Skill\(([^)]+)\)/g,
|
|
/\binvoke\s+([a-z][a-z0-9-]*)/gi,
|
|
/(?<![\p{L}\p{N}_])вызови\s+([a-z][a-z0-9-]*)/giu,
|
|
/(?<![\p{L}\p{N}_])делай\s+([a-z][a-z0-9-]*)/giu,
|
|
];
|
|
|
|
function normSkill(name) {
|
|
return String(name || '').trim().toLowerCase();
|
|
}
|
|
|
|
function itemText(item) {
|
|
return item.content ?? item.text ?? '';
|
|
}
|
|
|
|
export function extractSkillMentions(todoItems) {
|
|
const out = [];
|
|
for (const item of todoItems || []) {
|
|
const text = itemText(item);
|
|
const status = item.status || 'pending';
|
|
const found = new Set();
|
|
for (const pat of SKILL_MENTION_PATTERNS) {
|
|
const re = new RegExp(pat.source, pat.flags);
|
|
for (const m of String(text).matchAll(re)) {
|
|
// superpowers: and Skill() use m[0] / m[1] respectively
|
|
// invoke/вызови/делай capture group 1 for skill name
|
|
const val = m[1] !== undefined ? m[1] : m[0];
|
|
found.add(normSkill(val));
|
|
}
|
|
}
|
|
for (const skill_name of found) out.push({ skill_name, text, status });
|
|
}
|
|
return out;
|
|
}
|
|
|
|
export function extractSkillToolCalls(transcript) {
|
|
const out = [];
|
|
for (const rec of transcript || []) {
|
|
if (rec.type === 'tool_use' && rec.name === 'Skill') {
|
|
const name = rec.input?.skill ?? rec.input?.command ?? '';
|
|
out.push({ skill_name: normSkill(name), tool_use_id: rec.id ?? null });
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
export function skillNameMatches(mention, actualName) {
|
|
const a = normSkill(mention);
|
|
const b = normSkill(actualName);
|
|
if (a === b) return true;
|
|
// mention without namespace matches actual with namespace suffix
|
|
if (b.endsWith(':' + a)) return true;
|
|
if (a.endsWith(':' + b)) return true;
|
|
return false;
|
|
}
|
|
|
|
export function verifyClaims(skillMentions, actualSkillCalls) {
|
|
const mismatches = [];
|
|
for (const mention of skillMentions) {
|
|
const matched = (actualSkillCalls || []).some((c) => skillNameMatches(mention.skill_name, c.skill_name));
|
|
if (!matched) mismatches.push(mention);
|
|
}
|
|
return { mismatches };
|
|
}
|
|
|
|
export function detectErasure(historySnapshots) {
|
|
const erased = [];
|
|
for (const snap of historySnapshots || []) {
|
|
const removed = snap.diff?.removed || [];
|
|
for (const item of removed) {
|
|
const mentions = extractSkillMentions([item]);
|
|
if (mentions.length > 0) erased.push({ snapshot_ts: snap.ts, item, mentions });
|
|
}
|
|
}
|
|
return { erased };
|
|
}
|
|
|
|
export function hardSyncCheck(skillMentions, actualSkillCalls) {
|
|
for (const mention of skillMentions) {
|
|
if (mention.status !== 'completed') continue;
|
|
const wasInvoked = (actualSkillCalls || []).some((c) => skillNameMatches(mention.skill_name, c.skill_name));
|
|
if (!wasInvoked) {
|
|
return {
|
|
action: 'hard_block_next_mutating',
|
|
reason: `v4.1 TodoWrite hard sync: item "${mention.text}" marked completed claims Skill(${mention.skill_name}) invoked, но Skill tool call отсутствует. Либо invoke Skill сейчас, либо edit TodoWrite чтобы remove false claim.`,
|
|
};
|
|
}
|
|
}
|
|
return { action: 'allow' };
|
|
}
|