ClaudeMods
☰
ZH-TW
● 0 人在線上 · 瀏覽 0 次
贊助提交作品
GitHub 儲存庫 · 發布者 TransmuteLabs

catalyst-refusal-watch

在服務篩選拒絕發生的當下顯示它們,並加入規則要求模型回報停止,而不是靜默丟棄工作。

TransmuteLabs@TransmuteLabs

TransmuteLabs/Catalyst/tree/main/plugins/catalyst-refusal-watch

已翻譯

關於這個 mod

catalyst-refusal-watch 追蹤 TransmuteLabs/Catalyst 的 plugins/catalyst-refusal-watch。它 hook turn.step、turn.complete、tool.call(TaskStop)、prompt.context、session.start 與 session.end,偵測 refusal stopReason,發出 toast 與 transcript 列,為 TaskStop 通知維護 30 分鐘視窗,並可在 CLAUDE_REFUSAL_WATCH 開關後注入 refusalHandling 規則。它不改變引擎行為,也不注入拒絕回應;透過已驗證的 .claude-plugin/plugin.json 與 hooks register 模組發布。

安裝

請先查看作者 README,確認 marketplace 與外掛名稱;指令可能隨儲存庫結構而變動。

claude plugin marketplace add TransmuteLabs/Catalyst
claude plugin install catalyst-refusal-watch
原文 / README

catalyst-refusal-watch

Делает видимым каждый обрыв ответа фильтром сервиса в момент обрыва и добавляет модели правило: сообщить об остановке и не бросать линию работы молча.

Плагин не меняет поведение движка при обрыве и не трогает текст, который движок вставляет модели. Отказов и deny нет — только видимость.

Что делает

  • turn.step отдаёт чанки и результат без изменений и в том же порядке. Если stopReason чанка stop или результата шага — refusal, тост и строка транскрипта (модели она не уходит) показываются в момент распознавания — до чтения часов и не дожидаясь конца шага; счётчик растёт в момент распознавания, статус сессии — после ответа часов, а при отмене диспатча раньше ответа — по местному времени процесса; время, не узнанное ни от часов, ни местным, в статусе — --:--, дедупу хода оно не мешает; отказ местного времени или значение, не являющееся конечным числом — строка канала clock в debug, тост от него не зависит; обрыв, чья сессия закончилась (session.end) после входа хука, статус не ставит.
  • turn.complete при reason: "refusal" (ход оборван и повтора не было) сообщает отдельно. Если последний шаг того же хода (agentId и turnId) уже сообщил об обрыве, тост всё равно есть, а счётчик, окно и «последний» второй раз не меняются; вернувшийся шаг хода без обрыва или конец хода, у которого поле reason прочиталось (в том числе отсутствующее), это снимает; шаг, оборванный исключением, и конец хода с нечитаемой причиной запись хода не снимают. Нечитаемые agentId или turnId запись хода не ставят и не снимают; такой обрыв считается отдельно.
  • tool.call инструмента TaskStop в окне 30 минут после обрыва показывает тост с id задачи только при успехе ядра: result — запись вывода TaskStop со строковыми message, task_id, task_type, command — отсутствует или строка, нет deny, isError отсутствует или false. Вызов не блокируется и вне окна ничем не отмечается. Остановка, пришедшая, пока окно открывается (обрыв объявлен, часы ещё не ответили), считается внутри окна. Окно, метка открытия и время берутся на момент входа вызова. Время входа — местное время процесса, без ожидания: если оно внутри окна, для решения часы не читаются (запись журнала после тоста читает их как обычно). Без окна местное время решения не даёт. Окно с неизвестным концом (время обрыва не узнано ни от часов, ни местным временем) считается открытым: остановка внутри, для решения часы не читаются (запись журнала после тоста читает их как обычно). Иначе чтение часов выдаётся до ответа ядра, и исход решает его ответ. Остановка, начатая до обрыва, и остановка, чья сессия закончилась до её ответа, тоста не дают. Без окна и метки на входе часы не читаются. Неизвестное время входа (местное время не число или бросило, а часы отказали) считается внутри окна: лишний тост дешевле пропущенного. Метка открытия активна, пока местное время процесса не ушло вперёд от момента обрыва на 30 минут; перевод местных часов назад её не снимает; метка, чей момент неизвестен (местное время не число или бросило), по времени не снимается — только своим обрывом или концом сессии (session.end); нечисловое местное время входа метку не снимает. Порядок ответов часов не важен: окно кончается через 30 минут после наибольшего известного времени обрыва, «последний» — обрыв с наибольшим известным временем; обрыв с неизвестным временем держит конец окна неизвестным, пока после него не начат обрыв с известным временем. Без next.signal (вне контракта хоста) зависшие часы держат ответ остановки.
  • prompt.context добавляет запасной блок refusalHandling только при включённом CLAUDE_REFUSAL_WATCH, отсутствии подтверждённого паспортом системного носителя и отсутствии полного RULE_TEXT в любом блоке. Дубликаты собственного имени удаляются: первый блок с полным каноном (либо первый именованный, если канона среди них нет) сохраняется на позиции первого именованного блока. При вставке stale-текст заменяется; порядок прочих блоков не меняется.
  • session.start один раз на окружение модуля читает паспорт и выполняет awaited-пробу $.settings.read(); отдельный settings.read наблюдает собственный вложенный вызов. Успешная проба без этого наблюдения даёт одну D4-строку при включённом правиле; отказ пробы сообщает «доставка не подтверждена наблюдением».
  • session.end обнуляет счётчик «за сессию»: session.start на /clear не приходит (контракт), а session.end приходит на каждый конец сессии, включая /clear и resume. Паспорт, вердикт пробы и latch диагностик сохраняются; fresh load/reload с новым register повторяет инициализацию.

Выключатель CLAUDE_REFUSAL_WATCH

Контракт объявлен этим модом; системный сплайс шага 26 использует тот же выключатель. Читатель — $.env.get("CLAUDE_REFUSAL_WATCH"). Значение обрезается по краям и переводится в нижний регистр: 1, true, yes, on включают правило; отсутствие, пустая строка, 0, false, no, off выключают. Неизвестное значение не вставляет текст и даёт именованную диагностику. Без явного включения мод не добавляет правило; этот выключатель не отключает уведомления об обрывах.

Паспорт выбирается из раскрытого HOME-каталога $HOME/.local/share/catalyst-cc/passports/: ровно одно имя <session.version().version>-<platform>.json, где platform — [a-z0-9_]+. Платформа модом не вычисляется. Нет каталога/совпадений — fallback; несколько совпадений — fallback и passport-ambiguous. Поля version/platform сверяются с именем. Skip требует step26.applied === true, совпадающий rule_sha256 и совпадающие size/mtime канонического $HOME/.local/share/claude/versions/<version>. Нет канонического файла либо stat расходится — fallback и одна passport-stale. Иные отказы чтения/разбора/проверки не доказывают доставку и именованно диагностируются. Паспорт не доказывает final model payload; иной файл запуска к нему не привязан.

Журнал — ключи log:<время>:<номер>:<метка> в $.store, по одному на запись, не больше 100 последних (порядок = порядок вставки). Старый ключ log (массив модуля до 0.1.3) не читается и не изменяется; в журнал и в предел 100 он не входит. Обработчик ждёт запись не больше 5 с; дольше — строка в debug, запись дописывается в фоне. Отказ clock.now — строка канала clock в debug; время берётся из местного времени процесса, ожидание записи идёт как обычно. Отказ clock.sleep, пока диспатч не отменён, — строка канала clock и «ожидание записи снято отказом часов; запись не подтверждена», обработчик дальше не ждёт. Когда ожидание записи снято отменой диспатча, строка — «ожидание записи снято отменой диспатча; запись не подтверждена»; если sleep при этом отказал не причиной отмены, добавляется строка канала clock. Если признак отмены сигнала нечитаем, а отмена диспатча не зафиксирована раньше, строка — «ожидание записи снято отменой или отказом часов (сигнал нечитаем); запись не подтверждена», а отказ sleep называется строкой канала clock. Каждое ожидание обработчика — часов и записи — кончается с отменой диспатча (next.signal): время берётся из местного времени процесса; строка «ожидание записи снято отменой диспатча; запись не подтверждена» пишется только там, где запись начата; отказ sleep молчит только причиной самой отмены. Обрыв, чья сессия закончилась (session.end) после входа хука, в счётчик, окно, метку открытия и статус новой сессии не входит; запись в журнал остаётся. Каждый канал доставки изолирован: его отказ не прерывает шаг, ход или вызов.

Зубы

Прогоны #569 — только Linux, с отдельными HOME, CLAUDE_CONFIG_DIR и TMPDIR (последний вне HOME), внутри scope с MemoryMax=4G и MemorySwapMax=0. Не использовать живой HOME: хост выполняет уборку versions при старте. PLUGIN_DIR — копия этого каталога; файл версии не копируется и не меняется.

CLAUDE_BIN=/Users/maratkarimov/.local/share/claude/versions/2.1.285
"$CLAUDE_BIN" --version # требуется 2.1.285
CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 "$CLAUDE_BIN" plugin test "$PLUGIN_DIR"
CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 "$CLAUDE_BIN" plugin validate "$PLUGIN_DIR"

tests/sec-default-569.test.ts — 49 зубов выключателя, паспорта, локального канона, D4 и состояния окружения, включая paired host-chain с явным prepend и загрузчик 285. tests/parity.test.ts — один зуб SHA-256 полных UTF-8 байт и единственного пина. Вместе с исходными 160 это 210 тестов. Прямые callbacks не доказывают доставку по тирам; явный prepend не доказывает автоматическую посадку или final payload.

Кросс-репозиторный сторож запускается на Linux:

CATALYST_PATCH_KIT=/absolute/path/to/tweakcc-patch.js bash tests/scripts/check-splice-parity.sh

Нет кита, параметра либо REFUSAL_RULE — НЕ ИЗМЕРЕНО, RC=2; совпадение обоих литералов и пина — PASS, RC=0; расхождение — FAIL, RC=1.

tests/behavior.test.ts — 160 тестов официального стенда: текст и счётчик обрыва шага, тишина на end_turn, прозрачность чанков, категория хода без повтора, окно TaskStop, единственность блока правила, отказ тоста, дедуп счётчика по ходу, обрезка журнала, строка журнала без пустых полей, закрытие нижнего потока.

  • T14: чанк stop с refusal при результате end_turn даёт ровно один тост.
  • T15: stopReason: "refusal" только у результата, без чанка stop, даёт ровно один тост.
  • T16: тост об остановке задачи есть на 1 799 999 мс окна и нет ровно на 1 800 000.
  • T17: счётчик растёт на другом turnId и после шага без обрыва; шаг часов назад не пересчитывает.
  • T18: текст правила содержит обязанные фразы, блок refusalHandling длиннее 300.
  • T19: уведомление об остановке задачи печатается только при успешной остановке и называет агента.
  • T20: два одновременных обрыва оставляют в журнале две записи.
  • T21: отказ часов не глушит тост, в статусе время HH:MM.
  • T22: нечисловые часы открывают окно по Date.now(), в статусе нет NaN.
  • T23: ошибка нижнего потока после обрыва доходит наверх тем же объектом, тост один.
  • T24: отмена сверху после обрыва даёт тост и закрывает нижний поток.
  • T25: throw сверху доходит до нижнего генератора, восстановленный чанк возвращается.
  • T26: бросок next() у итератора не вызывает return().
  • T27: запись дедупа хода кончается его complete.
  • T28: статус содержит модель и локальное HH:MM; отказ тоста пишется с { to: "debug" }.
  • T29: session.end обнуляет счётчик сессии и возвращает результат next; окно TaskStop тоже закрывается (T39).
  • T30: результат без поля done всё равно читается как чанк.
  • T31: наблюдатель читает done и value результата ровно по одному разу.
  • T32: наблюдатель держит функцию next, взятую при входе, до конца делегирования.
  • T33: stopReason: "refusal" из нижнего return() тоже даёт уведомление.
  • T34: зависшая запись не держит вызывающего дольше 5 с и не затирает другую; в debug строка «запись дольше 5000 мс».
  • T35: запись, висящая через session.end, не теряется: в журнале остаются обе.
  • T36: шаг часов назад не выметает чужую запись дедупа.
  • T37: аргумент next(v) сверху доходит до нижнего next.
  • T38: отказ store.set не мешает следующей записи; в debug строка канала store.
  • T39: после session.end тоста об остановке задачи нет.
  • T40: предела по числу ключей нет.
  • T41: отказанный TaskStop без окна часов не читает.
  • T42: нечисловое значение часов не становится временем записи.
  • T43: успех TaskStop — только запись вывода TaskStop (message/task_id/task_type строками), без deny/isError; массив, пустой и неполный объект тоста не дают.
  • T44: поздний set не затирает запись, которая прошла, пока он ждал.
  • T45: отказ sleep отпускает вызывающего сразу, запись дописывается в фоне; в журнале оба хода.
  • T46: предел 5 с считается от начала каждого вызывающего, не общий на модуль.
  • T47: 105 записей оставляют в журнале 100 последних, первые пять ключей удалены.
  • T48: старый массив log не трогается, новые записи идут по своим ключам.
  • T49: нет throw — закрытие через return, значение закрытия не читается, наверх TypeError.
  • T50: stopReason: "refusal" в результате закрытия тоста не даёт.
  • T51: 1001 ход и complete первого дают счёт 1001; снимается только его ключ.
  • T52: геттер value читается ровно один раз на результат.
  • T53: TaskStop с isError: 1 тоста не даёт.
  • T54: NaN и Infinity от часов не становятся временем записи.
  • T55: TaskStop с result: undefined тоста не даёт.
  • T56: нет throw и нет return — оба геттера undefined, сверху выходит TypeError.
  • T57: старый log не входит в предел 100 записей.
  • T58: закрытие без throw зовёт нижний return без аргументов.
  • T59: невызываемый нижний return (не функция) отдаётся хосту как есть.
  • T60: TaskStop с result: null или строкой тоста не даёт.
  • T61: взведённый isError при полной записи вывода тоста не даёт; isError: false — даёт.
  • T62: закрытие зовёт нижний return, даже если у функции своё свойство apply.
  • T63: next и throw доходят до нижних методов, даже если у функций своё свойство call.
  • T64: бросивший геттер return не оставляет флаг закрытия.
  • T65: тост об обрыве выходит в момент чанка stop, не дожидаясь конца нижнего потока, и не повторяется.
  • T66: command — отсутствует или строка; число или null тоста об остановке задачи не дают.
  • T67: каждое строковое поле записи проверяется отдельно: без message, без task_id или массив — тоста нет.
  • T68: isError 0, null или "" при полной записи тоста не дают; isError: false — даёт.
  • T69: next и throw доходят до нижних методов, даже если у функций своё свойство apply.
  • T70: закрытие зовёт нижний return, даже если у функции своё свойство call.
  • T71: два чанка stop с refusal в одном потоке дают ровно один тост.
  • T72: без следующего next тост и строка транскрипта есть сразу при чанке stop, статус ждёт часы.
  • T73: зависшие часы держат шаг только до отмены диспатча; окно открывается по Date.now().
  • T74: остановка задачи, пока окно открывается, даёт тост.
  • T75: метка открытия окна снимается, когда окно поставлено: после окна тоста нет.
  • T76: обрыв, чей учёт пережил session.end, не держит окно и не считается в новой сессии; запись в журнале есть.
  • T77: turn.complete ждёт часы только пока жив диспатч.
  • T78: отмена при зависшей записи отпускает вызывающего и называет причину, а не отказ часов; sleep получает тот же сигнал; отказ sleep молчит только причиной отмены.
  • T79: отмена отпускает зависшую запись, даже если sleep сигнал не слушает.
  • T80: остановка, начатая до обрыва, после ответа не сравнивается с окном этого обрыва; начатая после — даёт тост.
  • T81: остановка, вошедшая без окна, часов не читает ни на входе, ни после ответа ядра; окно, открытое во время её ожидания, исход не решает.
  • T82: остановка, пережившая session.end, по окну новой сессии тоста не даёт.
  • T83: turn.complete, чей next вернулся после session.end, даёт тост, но ни статуса, ни окна.
  • T84: шаг, начатый до session.end, не ставит метку открытия и статус в новой сессии.
  • T85: снятие метки одного обрыва не снимает метку другого, ещё ждущего часы.
  • T86: метка открытия живёт до 1 799 999 мс местного времени и снята на 1 800 000.
  • T87: при уже отменённом сигнале на ожидаемый промис всё равно ставится подписка.
  • T88: бросок onAbort отклоняет ожидание — и при уже отменённом сигнале, и из слушателя.
  • T89: ожидание, решённое своим промисом, снимает слушатель отмены.
  • T90: отмена после ответа промиса ничего не вызывает; слушатель ставится однократным.
  • T91: при отменённом сигнале посторонний отказ sleep — отказ часов.
  • T92: строка отмены не утверждает, что запись продолжается в фоне.
  • T93: отказ sleep без отмены называет неподтверждённую запись.
  • T94: остановка, чей next вернулся после session.end, после этого часов не читает и тоста не даёт.
  • T95: остановка, вошедшая при открытом окне, тоста не даёт, если session.end пришёл, пока она читала часы.
  • T96: та же остановка без session.end даёт ровно один тост.
  • T97: остановка, вошедшая внутри окна, даёт тост, даже если ядро ответило после края окна.
  • T98: зависшее и отменённое чтение часов на входе остановки решается местным временем входа, а не более поздним.
  • T99: отказ местного времени на входе остановки не отнимает у вызова его результат.
  • T100: перевод местных часов назад меньше чем на окно метку открытия не снимает.
  • T101: при уже отменённом сигнале у ожидаемого промиса есть обработчик отказа.
  • T102: отказ промиса с бросающим onAbort снимает слушатель отмены.
  • T103: при отменённом сигнале отказ sleep новым AbortError не считается отказом часов.
  • T104: отменённый сигнал без подписки называет в строке журнала отмену диспатча, не часы.
  • T106: нечисловое местное время на входе остановки не снимает меток и считается внутри.
  • T107: бросок местного времени на входе остановки не отменяет решения по метке открытия.
  • T108: остановка, чьё местное время входа внутри окна, даёт тост, даже если часы ответили бы за краем.
  • T109: без местного времени и без часов на входе остановка считается внутри окна.
  • T110: остановка с местным временем входа внутри окна не читает часов для решения.
  • T111: без окна отрицательное местное время тоста не даёт и часов не читает.
  • T112: обрыв, чьё время не узнано, открывает окно с неизвестным концом; остановка в нём даёт тост и не читает часов для решения.
  • T113: окно, чей конец равен нулю, остаётся окном.
  • T114: метка, поставленная в неизвестное время, известным временем входа не выметается.
  • T115: перевод местных часов назад больше чем на окно метку открытия не снимает.
  • T116: бросок местного времени при постановке метки не отнимает тост обрыва.
  • T117: обрыв с неизвестным временем не пишет NaN в статус.
  • T118: обрыв с неизвестным временем сводит complete своего хода.
  • T119: обрыв, чьи часы ответили позже более нового, не отодвигает окно и «последний» в статусе назад.
  • T120: более старый шаг хода, чьи часы ответили позже, не ломает дедуп хода.
  • T121: нестроковое пояснение отказа всё равно объявляет ход.
  • T122: нестроковые поля события всё равно дают текст тоста и статуса.
  • T123: сигнал, чей слушатель не читается, не теряет учёт обрыва.
  • T124: бросок сразу после постановки метки открытия всё равно снимает метку.
  • T125: нечисловое местное время названо строкой канала clock в debug.
  • T126: более новый обрыв с меньшим временем не закрывает окно более старого.
  • T127: обрыв с неизвестным временем держит окно открытым, пока после него не начат обрыв с известным временем.
  • T128: complete, сведённый со шагом своего хода, сохраняет модель шага в статусе.
  • T129: шаг хода без обрыва снова вооружает счётчик complete этого хода.
  • T130: complete с любой причиной снимает запись дедупа своего хода.
  • T131: тот же turnId у другого агента — другой ход.
  • T132: шаги хода, отвечающие часам не по порядку, всё равно сводят его complete.
  • T133: имена инструментов не-строки всё равно дают текст тоста.
  • T134: поля журнала хранятся текстом.
  • T135: бросающий геттер поля отказа не теряет тост и результат хода.
  • T136: бросающий геттер сигнала не теряет учёт шага.
  • T137: результат TaskStop с бросающим полем возвращается как есть.
  • T138: чанк с бросающим полем проходит без изменений.
  • T139: результат контекста с бросающим полем blocks возвращается как есть.
  • T140: сигнал, читаемый дважды, не ломает ожидание.
  • T141: бесконечное местное время названо строкой канала clock.
  • T142: turnId, чей текст бросает, всё равно даёт строку транскрипта.
  • T143: reset снимает шов fault.
  • T144: шов fault не срабатывает без метки открытия.
  • T145: сломанное местное время и отказ часов называют местное время один раз.
  • T146: сведённый complete, пришедший раньше ответа часов шага, не показывает счёт без этого обрыва.
  • T147: время обрыва 0 и отрицательное — известное время: окно кончается через 30 минут после него.
  • T148: шаг, оборванный исключением, запись дедупа хода не снимает.
  • T149: при отказе часов и местного времени запись журнала хранит время null.
  • T150: шаг без turnId запись дедупа не ставит — complete без turnId считается.
  • T151: неконечный номер шага хранится текстом.
  • T152: конец хода снимает агента из карты дедупа, когда его ходов не осталось.
  • T153: нечитаемый reason не снимает запись дедупа хода.
  • T154: нечитаемый признак отмены назван в строке ожидания записи.
  • T155: нечитаемый agentId на конце хода не снимает запись дедупа главного хода.
  • T156: вернувшийся шаг с нечитаемым agentId не снимает запись дедупа главного хода.
  • T157: шаг отказа с нечитаемым agentId не ставит запись дедупа — его complete считается отдельно.
  • T158: конец отказа с нечитаемым agentId считается отдельно и запись дедупа не снимает.
  • T159: нечитаемый reason отменённого сигнала не выпускает AbortError отмены в канал clock.
  • T160: нечитаемый reason и отказ undefined — отказ часов, строка канала clock есть.
  • T161: бросающий геттер name у отказа, не равного причине, не прячет отказ часов.

Validate из команд выше перечисляет две отдельные регистрации session.start / settings.read и вызов $.settings.read; это свидетель загрузчика, не tier-delivery.

更多類似作品