/**
 * ============================================================
 *  plugins/ralph-wiggum/src/sentinels.ts —— LLM 输出哨兵协议
 * ------------------------------------------------------------
 *  Ralph 通过"在回答里输出特定标签"与循环驱动（plugin.ts）通信：
 *
 *    <promise>DONE</promise> / <promise>COMPLETE</promise> / <promise>GOAL_COMPLETE</promise>
 *    （或用户经 `--completion-promise "<短语>"` 指定的自定义短语）
 *      → 当前轮 LLM 认为目标完成；驱动进入验证门（--verify）确认后才真正退出。
 *
 *    <PROMISE>NEED_REPLAN</PROMISE>
 *      → 当前方案走不通、需要换打法（**目标不变**）；驱动强转回 PLAN 阶段。
 *
 *    <PROMISE>NEED_GOAL_REVISION</PROMISE>
 *      → **目标本身**有问题，无法在不改目标的前提下完成；驱动**干净终止**，请人工修订目标
 *        （区别于 NEED_REPLAN：那是换方案，这是换目标）。
 *
 *    <learning>一句话</learning>（可多次）
 *      → LLM 主动记录的踩坑 / 教训；驱动**每轮**提取并追加到 learnings.md，
 *        让经验跨 fresh-context 迭代存活——这是 Ralph 能收敛的关键。
 *
 *  哨兵集中在这里方便审计与扩展；匹配全部 case-insensitive。
 *  ⚠️ rich 插件路径下 commands/ralph-loop.md 的 prompt body **不会**喂给 LLM
 *     （pluginRunner 命中 runCommand 即接管），故以上协议由 GoalState.composeContext
 *     注入 prompt，本文件只负责"解析 LLM 回吐的标签"。
 * ============================================================
 */
/** 写死的完成短语集合（不带 --completion-promise 时只认这三个，逐字节兼容旧行为）。 */
const BUILTIN_COMPLETE_RE = /<promise>(?:COMPLETE|DONE|GOAL_COMPLETE)<\/promise>/i;
const NEED_REPLAN_RE = /<PROMISE>NEED_REPLAN<\/PROMISE>/i;
const NEED_GOAL_REVISION_RE = /<PROMISE>NEED_GOAL_REVISION<\/PROMISE>/i;
const LEARNING_RE = /<learning>([\s\S]*?)<\/learning>/gi;
/** 转义正则元字符，让用户自定义的 promise 短语按字面量匹配（含空格 / 标点也安全）。 */
function escapeRegExp(s) {
    return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
/**
 * 构造完成哨兵判定器。
 * - 不传 / 空白 customPhrase → 只认写死集合（与旧行为逐字节一致）。
 * - 传 customPhrase → 写死集合 ∪ `<promise><customPhrase></promise>`（**超集**，向后兼容）。
 *   customPhrase 内外空白裁剪；正则元字符转义；标签内允许首尾空白；大小写不敏感。
 *
 * 设计：始终接受写死集合，所以即便用户设了自定义短语，LLM 输出 `<promise>DONE</promise>`
 *      仍然有效——composeContext 因此可以无脑教 LLM 用 DONE，不必知道自定义短语是什么。
 */
export function makeCompletionMatcher(customPhrase) {
    const phrase = customPhrase?.trim();
    if (!phrase) {
        return (text) => BUILTIN_COMPLETE_RE.test(text);
    }
    const customRe = new RegExp(`<promise>\\s*${escapeRegExp(phrase)}\\s*<\\/promise>`, 'i');
    return (text) => BUILTIN_COMPLETE_RE.test(text) || customRe.test(text);
}
/** 旧调用兼容：等价于 makeCompletionMatcher()(text)，只认写死集合。 */
export function hasCompleteSentinel(text) {
    return BUILTIN_COMPLETE_RE.test(text);
}
export function hasNeedReplanSentinel(text) {
    return NEED_REPLAN_RE.test(text);
}
export function hasNeedGoalRevisionSentinel(text) {
    return NEED_GOAL_REVISION_RE.test(text);
}
/**
 * 提取 LLM 本轮输出里所有 `<learning>…</learning>` 文本。
 * 多空白折叠成单空格、去首尾、丢弃空项；无标签返回空数组。
 */
export function extractLearnings(text) {
    const out = [];
    for (const m of text.matchAll(LEARNING_RE)) {
        const lesson = m[1].replace(/\s+/g, ' ').trim();
        if (lesson)
            out.push(lesson);
    }
    return out;
}
